diff --git a/common/services/utils/translation_services.py b/common/services/utils/translation_services.py
index 4f0e273..437ebba 100644
--- a/common/services/utils/translation_services.py
+++ b/common/services/utils/translation_services.py
@@ -1,4 +1,6 @@
import json
+import copy
+import re
from typing import Dict, Any, Optional
from flask import session
@@ -50,8 +52,8 @@ class TranslationServices:
if isinstance(config_data, str):
config_data = json.loads(config_data)
- # Maak een kopie van de originele data om te wijzigen
- translated_config = config_data.copy()
+ # 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')
@@ -65,71 +67,124 @@ class TranslationServices:
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
- if 'name' in field_data and field_data['name']:
- # Gebruik context indien opgegeven, anders description_context
+ # 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
- translated_name = cache_manager.translation_cache.get_translation(
- text=field_data['name'],
- target_lang=target_language,
- source_lang=source_language,
- context=field_context
- )
- if translated_name:
- translated_config[field_config][field_name]['name'] = translated_name.translated_text
+ t = safe_translate(field_data['name'], field_context)
+ if t:
+ translated_config[field_config][field_name]['name'] = t
- if 'title' in field_data and field_data['title']:
- # Gebruik context indien opgegeven, anders description_context
+ if 'title' in field_data and is_nonempty_str(field_data.get('title')):
field_context = context if context else description_context
- translated_title = cache_manager.translation_cache.get_translation(
- text=field_data['title'],
- target_lang=target_language,
- source_lang=source_language,
- context=field_context
- )
- if translated_title:
- translated_config[field_config][field_name]['title'] = translated_title.translated_text
+ 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 field_data['description']:
- # Gebruik context indien opgegeven, anders description_context
+ if 'description' in field_data and is_nonempty_str(field_data.get('description')):
field_context = context if context else description_context
- translated_desc = cache_manager.translation_cache.get_translation(
- text=field_data['description'],
- target_lang=target_language,
- source_lang=source_language,
- context=field_context
- )
- if translated_desc:
- translated_config[field_config][field_name]['description'] = translated_desc.translated_text
+ 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 field_data['context']:
- translated_ctx = cache_manager.translation_cache.get_translation(
- text=field_data['context'],
- target_lang=target_language,
- source_lang=source_language,
- context=context
- )
- if translated_ctx:
- translated_config[field_config][field_name]['context'] = translated_ctx.translated_text
+ 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 de waarden niet leeg zijn.
- if 'allowed_values' in field_data and field_data['allowed_values']:
+ # 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']:
- translated_allowed_value = cache_manager.translation_cache.get_translation(
- text=allowed_value,
- target_lang=target_language,
- source_lang=source_language,
- context=context
- )
- translated_allowed_values.append(translated_allowed_value.translated_text)
+ 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
diff --git a/config/prompts/globals/translation_with_context/1.0.0.yaml b/config/prompts/globals/translation_with_context/1.0.0.yaml
index ec01348..a05eb85 100644
--- a/config/prompts/globals/translation_with_context/1.0.0.yaml
+++ b/config/prompts/globals/translation_with_context/1.0.0.yaml
@@ -9,8 +9,11 @@ content: >
'{context}'
- Do not translate text in between double square brackets, as these are names or terms that need to remain intact.
- Remove the triple quotes in your translation!
+ These are best practices you should follow:
+
+ - Do not translate text in between double square brackets, as these are names or terms that need to remain intact. Remove the square brackets in the translation!
+ - We use inline tags (Custom HTML/XML-like tags). Ensure the tags themself are not translated and remain intact in the translation. The text inbetween the tags should be translated. e.g. "Terms & Conditions" translates in Dutch to Gebruiksvoorwaarden
+ - Remove the triple quotes in your translation!
I only want you to return the translation. No explanation, no options. I need to be able to directly use your answer
without further interpretation. If more than one option is available, present me with the most probable one.
diff --git a/config/prompts/globals/translation_without_context/1.0.0.yaml b/config/prompts/globals/translation_without_context/1.0.0.yaml
index c895031..95477f9 100644
--- a/config/prompts/globals/translation_without_context/1.0.0.yaml
+++ b/config/prompts/globals/translation_without_context/1.0.0.yaml
@@ -6,8 +6,11 @@ content: >
into '{target_language}'.
- Do not translate text in between double square brackets, as these are names or terms that need to remain intact.
- Remove the triple quotes in your translation!
+ These are best practices you should follow:
+
+ - Do not translate text in between double square brackets, as these are names or terms that need to remain intact. Remove the square brackets in the translation!
+ - We use inline tags (Custom HTML/XML-like tags). Ensure the tags themself are not translated and remain intact in the translation. The text inbetween the tags should be translated. e.g. "Terms & Conditions" translates in Dutch to Gebruiksvoorwaarden
+ - Remove the triple quotes in your translation!
I only want you to return the translation. No explanation, no options. I need to be able to directly use your answer
without further interpretation. If more than one option is available, present me with the most probable one.
diff --git a/config/specialist_forms/globals/MINIMAL_PERSONAL_CONTACT_FORM/1.0.0.yaml b/config/specialist_forms/globals/MINIMAL_PERSONAL_CONTACT_FORM/1.0.0.yaml
index 0b62127..b238615 100644
--- a/config/specialist_forms/globals/MINIMAL_PERSONAL_CONTACT_FORM/1.0.0.yaml
+++ b/config/specialist_forms/globals/MINIMAL_PERSONAL_CONTACT_FORM/1.0.0.yaml
@@ -24,6 +24,11 @@ fields:
type: "boolean"
description: "Consent"
required: true
+ meta:
+ kind: "consent"
+ consentRich: "Ik Agree with the Terms and Conditions and the Privacy Statement of Ask Eve AI"
+ ariaPrivacy: "Open privacyverklaring in a modal dialog"
+ ariaTerms: "Open algemene voorwaarden in a modal dialog"
metadata:
author: "Josako"
date_added: "2025-07-29"
diff --git a/eveai_chat_client/static/assets/vue-components/ConsentRichText.vue b/eveai_chat_client/static/assets/vue-components/ConsentRichText.vue
new file mode 100644
index 0000000..c083d4a
--- /dev/null
+++ b/eveai_chat_client/static/assets/vue-components/ConsentRichText.vue
@@ -0,0 +1,94 @@
+
+
+
+ {{ node.label }}
+ {{ node.text }}
+
+
+
+
+
+
+
diff --git a/eveai_chat_client/static/assets/vue-components/FormField.vue b/eveai_chat_client/static/assets/vue-components/FormField.vue
index 336de27..4a2b130 100644
--- a/eveai_chat_client/static/assets/vue-components/FormField.vue
+++ b/eveai_chat_client/static/assets/vue-components/FormField.vue
@@ -104,14 +104,16 @@
>
{{ field.name }}
-
-
- {{ texts.consentPrefix }}
- {{ texts.privacyLink }}
- {{ texts.consentMiddle }}
- {{ texts.termsLink }}
- {{ texts.consentSuffix }}
-
+
+
*
@@ -180,9 +182,11 @@