Files
eveAI/common/services/utils/translation_services.py
Josako 2b04692fab - TRA-76 - Send Button color changes implemented
- TRA-72 - Translation of privacy statement and T&C
- TRA-73 - Strange characters in Tenant Make Name
- Addition of meta information in Specialist Form Fields
2025-09-15 17:57:13 +02:00

203 lines
11 KiB
Python

import json
import copy
import re
from typing import Dict, Any, Optional
from flask import session
from common.extensions import cache_manager
from common.utils.business_event import BusinessEvent
from common.utils.business_event_context import current_event
class TranslationServices:
@staticmethod
def translate_config(tenant_id: int, config_data: Dict[str, Any], field_config: str, target_language: str,
source_language: Optional[str] = None, context: Optional[str] = None) -> Dict[str, Any]:
"""
Vertaalt een configuratie op basis van een veld-configuratie.
Args:
tenant_id: Identificatie van de tenant waarvoor we de vertaling doen.
config_data: Een dictionary of JSON (die dan wordt geconverteerd naar een dictionary) met configuratiegegevens
field_config: De naam van een veld-configuratie (bijv. 'fields')
target_language: De taal waarnaar vertaald moet worden
source_language: Optioneel, de brontaal van de configuratie
context: Optioneel, een specifieke context voor de vertaling
Returns:
Een dictionary met de vertaalde configuratie
"""
config_type = config_data.get('type', 'Unknown')
config_version = config_data.get('version', 'Unknown')
span_name = f"{config_type}-{config_version}-{field_config}"
if current_event:
with current_event.create_span(span_name):
translated_config = TranslationServices._translate_config(tenant_id, config_data, field_config,
target_language, source_language, context)
return translated_config
else:
with BusinessEvent('Config Translation Service', tenant_id):
with current_event.create_span(span_name):
translated_config = TranslationServices._translate_config(tenant_id, config_data, field_config,
target_language, source_language, context)
return translated_config
@staticmethod
def _translate_config(tenant_id: int, config_data: Dict[str, Any], field_config: str, target_language: str,
source_language: Optional[str] = None, context: Optional[str] = None) -> Dict[str, Any]:
# Zorg ervoor dat we een dictionary hebben
if isinstance(config_data, str):
config_data = json.loads(config_data)
# Maak een deep copy van de originele data om te wijzigen en input-mutatie te vermijden
translated_config = copy.deepcopy(config_data)
# Haal type en versie op voor de Business Event span
config_type = config_data.get('type', 'Unknown')
config_version = config_data.get('version', 'Unknown')
if field_config in config_data:
fields = config_data[field_config]
# Haal description uit metadata voor context als geen context is opgegeven
description_context = ""
if not context and 'metadata' in config_data and 'description' in config_data['metadata']:
description_context = config_data['metadata']['description']
# Hulpfuncties
def is_nonempty_str(val: Any) -> bool:
return isinstance(val, str) and val.strip() != ''
def safe_translate(text: str, ctx: Optional[str]):
try:
res = cache_manager.translation_cache.get_translation(
text=text,
target_lang=target_language,
source_lang=source_language,
context=ctx
)
return res.translated_text if res else None
except Exception as e:
if current_event:
current_event.log_error('translation_error', {
'tenant_id': tenant_id,
'config_type': config_type,
'config_version': config_version,
'field_config': field_config,
'error': str(e)
})
return None
tag_pair_pattern = re.compile(r'<([a-zA-Z][\w-]*)>[\s\S]*?<\/\1>')
def extract_tag_counts(text: str) -> Dict[str, int]:
counts: Dict[str, int] = {}
for m in tag_pair_pattern.finditer(text or ''):
tag = m.group(1)
counts[tag] = counts.get(tag, 0) + 1
return counts
def tags_valid(source: str, translated: str) -> bool:
return extract_tag_counts(source) == extract_tag_counts(translated)
# Counters
meta_consentRich_translated_count = 0
meta_aria_translated_count = 0
meta_inline_tags_invalid_after_translation_count = 0
# Loop door elk veld in de configuratie
for field_name, field_data in fields.items():
# Vertaal name als het bestaat en niet leeg is (alleen strings)
if 'name' in field_data and is_nonempty_str(field_data['name']):
field_context = context if context else description_context
t = safe_translate(field_data['name'], field_context)
if t:
translated_config[field_config][field_name]['name'] = t
if 'title' in field_data and is_nonempty_str(field_data.get('title')):
field_context = context if context else description_context
t = safe_translate(field_data['title'], field_context)
if t:
translated_config[field_config][field_name]['title'] = t
# Vertaal description als het bestaat en niet leeg is
if 'description' in field_data and is_nonempty_str(field_data.get('description')):
field_context = context if context else description_context
t = safe_translate(field_data['description'], field_context)
if t:
translated_config[field_config][field_name]['description'] = t
# Vertaal context als het bestaat en niet leeg is
if 'context' in field_data and is_nonempty_str(field_data.get('context')):
t = safe_translate(field_data['context'], context)
if t:
translated_config[field_config][field_name]['context'] = t
# vertaal allowed_values als het veld bestaat en waarden niet leeg zijn (alleen string-items)
if 'allowed_values' in field_data and isinstance(field_data['allowed_values'], list) and field_data['allowed_values']:
translated_allowed_values = []
for allowed_value in field_data['allowed_values']:
if is_nonempty_str(allowed_value):
t = safe_translate(allowed_value, context)
translated_allowed_values.append(t if t else allowed_value)
else:
translated_allowed_values.append(allowed_value)
if translated_allowed_values:
translated_config[field_config][field_name]['allowed_values'] = translated_allowed_values
# Vertaal meta.consentRich en meta.aria*
meta = field_data.get('meta')
if isinstance(meta, dict):
# consentRich
if is_nonempty_str(meta.get('consentRich')):
consent_ctx = (context if context else description_context) or ''
consent_ctx = f"Consent rich text with inline tags. Keep tag names intact and translate only inner text. {consent_ctx}".strip()
t = safe_translate(meta['consentRich'], consent_ctx)
if t and tags_valid(meta['consentRich'], t):
translated_config[field_config][field_name].setdefault('meta', {})['consentRich'] = t
meta_consentRich_translated_count += 1
else:
if t and not tags_valid(meta['consentRich'], t) and current_event:
src_counts = extract_tag_counts(meta['consentRich'])
dst_counts = extract_tag_counts(t)
current_event.log_error('inline_tags_validation_failed', {
'tenant_id': tenant_id,
'config_type': config_type,
'config_version': config_version,
'field_config': field_config,
'field_name': field_name,
'target_language': target_language,
'source_tag_counts': src_counts,
'translated_tag_counts': dst_counts
})
meta_inline_tags_invalid_after_translation_count += 1
# fallback: keep original (already in deep copy)
# aria*
for k, v in list(meta.items()):
if isinstance(k, str) and k.startswith('aria') and is_nonempty_str(v):
aria_ctx = (context if context else description_context) or ''
aria_ctx = f"ARIA label for accessibility. Short, imperative, descriptive. Form '{config_type} {config_version}', field '{field_name}'. {aria_ctx}".strip()
t2 = safe_translate(v, aria_ctx)
if t2:
translated_config[field_config][field_name].setdefault('meta', {})[k] = t2
meta_aria_translated_count += 1
return translated_config
@staticmethod
def translate(tenant_id: int, text: str, target_language: str, source_language: Optional[str] = None,
context: Optional[str] = None)-> str:
if current_event:
with current_event.create_span('Translation'):
translation_cache = cache_manager.translation_cache.get_translation(text, target_language,
source_language, context)
return translation_cache.translated_text
else:
with BusinessEvent('Translation Service', tenant_id):
with current_event.create_span('Translation'):
translation_cache = cache_manager.translation_cache.get_translation(text, target_language,
source_language, context)
return translation_cache.translated_text