﻿<?xml version="1.0" encoding="utf-8"?>
<configset name="DeepLTranslations" author="CBR">
  <comment>2026-08-06, MDA: v1</comment>
  <requirements>
    <version-requirement>6.8.0.20</version-requirement>
  </requirements>
  <update />
  <references />
  <objects>
    <object class="StringProperty" alias="StringProperty_DeeLApiKey">
      <member name="propertyGroup">2</member>
      <member name="selectionType">None</member>
      <member name="scope">Global</member>
      <member name="propertyName">deepl_api_key</member>
      <member name="propertyCaption">DeepL API Key</member>
      <member name="masked">true</member>
    </object>
    <object class="StringProperty" alias="StringProperty_DeeLApiUrl">
      <member name="propertyGroup">2</member>
      <member name="selectionType">None</member>
      <member name="scope">Global</member>
      <member name="propertyName">deepl_api_url</member>
      <member name="propertyCaption">DeepL API URL</member>
      <member name="masked">false</member>
    </object>
    <object class="ClassSettings" alias="ClassSettings_OffeneSpesen7">
      <member name="entryID">ClassSettingsOpenExpenseDeepL</member>
      <member name="customPages">&lt;!-- =============== Open expense: ExpenseOutlayFurtherInfoPage =============== --&gt;
&lt;Page Override="ExpenseOutlayFurtherInfo"&gt;
    &lt;TextArea Name="TextAreaRemarks"
        Label="{Translate 'Remarks'}"
        Lines="3"
        ValueExpression="remarks"
        VisibleExpression="not(remarks.asstring='')"
        PlaceAfter="AdditionalFieldGroup" /&gt;
&lt;/Page&gt;</member>
      <member name="class">OpenExpense</member>
    </object>
    <object class="ClassSettings" alias="ClassSettings_OffeneAuslage8">
      <member name="entryID">ClassSettingsOpenOutlayDeepL</member>
      <member name="customPages">&lt;!-- =============== Open outlay: ExpenseOutlayFurtherInfoPage =============== --&gt;
&lt;Page Override="ExpenseOutlayFurtherInfo"&gt;
    &lt;TextArea Name="TextAreaRemarks"
        Label="{Translate 'Remarks'}"
        Lines="3"
        ValueExpression="remarks"
        VisibleExpression="not(remarks.asstring='')"
        PlaceAfter="AdditionalFieldGroup" /&gt;
&lt;/Page&gt;</member>
      <member name="class">OpenOutlay</member>
    </object>
    <object class="ClassSettings" alias="ClassSettings_OffeneLeistung6">
      <member name="entryID">ClassSettingsOpenServiceDeepL</member>
      <member name="customPages">&lt;!-- =============== Open service: ServiceFurtherInfoPage =============== --&gt;
&lt;Page Override="ServiceFurtherInfo"&gt;
    &lt;TextArea Name="TextAreaRemarks"
        Label="{Translate 'Remarks'}"
        Lines="3"
        ValueExpression="remarks"
        VisibleExpression="not(remarks.asstring='')"
        PlaceAfter="AdditionalFieldGroup" /&gt;
&lt;/Page&gt;</member>
      <member name="class">OpenService</member>
    </object>
    <object class="Script" alias="ScriptEintrag_Textübersetzen1">
      <member name="entryID">ScriptDeeplTranslateText</member>
      <member name="designation">DeepL: Translate text</member>
      <member name="classes">OpenOutlay,OpenService,OpenExpense</member>
      <member name="containerScript">false</member>
      <member name="scriptText"># coding: windows-1252
#
# Name: DeepL: Translate text
# SelectedObjectScript: Y
# Classes: OpenOutlay,OpenService,OpenExpense
# CondExpression: N
# ExtendedRights: N
#
# Description: Translates the text of the project item using DeepL.
#
# 2025-09-25, CBR: Created.
# 2026-08-06, MDA: Reviewed, changed, enhanced


import vtcapp
import module_deepl


def main(cost_item_list):
    """
    Main entry for function.
    """
    # "with" construction guarantees that a session is closed
    # even when any exception is raised inside of code
    with module_deepl.DeepL() as deepl:
        # Create list per language with the cost items, rejecting those with empty project.language
        languages = cost_item_list.evalocl("oclastype(ProjectCostItem).getProject.language.asstring-&gt;asSet"
                                        "-&gt;reject(s|s='')")
        # Preparation of dictionary with elements of "language_code": [cost item list]
        grouped_by_language = {}
        for language in languages:
            grouped_by_language[language] = []
        
        # Appending cost items to language lists
        for cost_item in cost_item_list:
            language = cost_item.evalocl("getProject.language.asstring")
            if not language:
                continue
            grouped_by_language[language].append(cost_item)

        # Execute translation per language
        for language, cost_items in grouped_by_language.items():
            texts = []
            for cost_item in cost_items:
                texts.append(cost_item.text)
            translations = deepl.translate(texts, language)

            # Replace old text if source language differs from target. Store original in remarks.
            for cost_item, translation in zip(cost_items, translations):
                if translation["detected_source_language"] == language:
                    continue
                if not cost_item.remarks:
                    cost_item.remarks = ""
                else:
                    cost_item.remarks += "\n"
                cost_item.remarks += ("{} - {}:\n({}) {}"
                                    .format(vtcapp.currentdate(),
                                            vtcapp.translate("Text before translation"),
                                            translation["detected_source_language"],
                                            cost_item.text))
                cost_item.text = translation["text"]


if (vtcapp.getpropertyvalue("deepl_api_url") and vtcapp.getpropertyvalue("deepl_api_key")):
    main(selectedobjects)
else:
    vtcapp.msgbox(vtcapp.translate("Please make sure that system settings 'DeepL API URL' and 'DeepL API Key' are set."))
</member>
      <member name="eventType">None</member>
      <member name="objectScript">false</member>
      <member name="extendedRights">false</member>
      <member name="selectedObjectsScript">true</member>
      <member name="platform">Python2</member>
    </object>
    <object class="Script" alias="ScriptEintrag_module_deepl2">
      <member name="entryID">ScriptModuleDeepl</member>
      <member name="designation">module_deepl</member>
      <member name="containerScript">false</member>
      <member name="scriptText"># coding: windows-1252
#
# Name: module_deepl
# Klassen:
# CondExpression:
# ObjectScript: N
# ContainerScript: N
# EventType:
# EventClass:
# EventMembers:
# ExtendedRights: Y
# ModulDescription: Provides functions to translate text using DeepL.
#
# 2025-09-24, CBR: Created.
# 2026-08-06, MDA: Reviewed, changed, enhanced


import vtcapp
import time
import requests


MAX_RETRIES = 3
SESSION_VERIFY_SSL = True
SESSION_STREAM = True


class DeepL():
    def __init__(self):
        self.api_url = vtcapp.getpropertyvalue("deepl_api_url")
        if not self.api_url:
            raise Exception("DeepL API URL missing.")

        self.api_key = vtcapp.getpropertyvalue("deepl_api_key")
        if not self.api_key:
            raise Exception("DeepL API KEY missing.")

        self.session = requests.Session()
        self.session.verify = SESSION_VERIFY_SSL
        self.session.stream = SESSION_STREAM
        self.translate_url = self.api_url.rstrip("/") + "/v2/translate"

    # two functions below are needed to make it possible
    # to work with "with"
    def __enter__(self):
        return self

    def __exit__(self, *_):
        self.session.close()

    def _post_translate(self, data):
        """
        Sends a POST request to the DeepL translate endpoint.
        Retries on 429/500, raises on other errors.
        """
        headers = {
            "Authorization": "DeepL-Auth-Key {}".format(self.api_key),
            "Content-type": "application/x-www-form-urlencoded",
            "Accept": "application/json",
        }

        retries = 0
        while True:
            response = self.session.post(self.translate_url,
                                         headers=headers,
                                         data=data,
                                         allow_redirects=False,
                                         timeout=(5, 15))
            if response.status_code in (429, 500):
                retries += 1
                if retries &gt; MAX_RETRIES:
                    raise Exception("DeepL: Request failed with status {}.".format(response.status_code))
                time.sleep(2 ** retries)  # 2s, 4s, 8s(, ...)
                continue
            break

        if response.status_code == 456:
            raise Exception("DeepL: Quota exceeded.")

        if response.status_code &lt; 200 or response.status_code &gt;= 300:
            error_msg = "DeepL: Unexpected response code {}.".format(response.status_code)
            # to make sure that a response is of a json type
            try:
                response_json = response.json()
            except Exception:
                response_json = {}
            if "message" in response_json:
                error_msg += " " + response_json["message"]
            raise Exception(error_msg)

        return response.json()

    def translate(self, texts, target_language):
        """
        Translates a list of texts to target_language.
        Returns a list of dicts with 'text' and 'detected_source_language'.
        """
        data = {
            "text": texts,
            "target_lang": target_language.upper(),
        }
        response = self._post_translate(data)
        return response["translations"]
</member>
      <member name="eventType">None</member>
      <member name="objectScript">false</member>
      <member name="extendedRights">false</member>
      <member name="selectedObjectsScript">false</member>
      <member name="platform">Python2</member>
    </object>
    <object class="TranslationItem" alias="TranslationItem_Übersetzung:Translatetext0">
      <member name="entryID">TranslationDeeplTranslateText</member>
      <member name="text">
        <mltext language="DE">DeepL: Text übersetzen</mltext>
        <mltext language="EN">DeepL: Translate text</mltext>
        <mltext language="FR">DeepL: Traduire le texte</mltext>
        <mltext language="IT">DeepL: Traduci il testo</mltext>
        <mltext language="DD">DeepL: Text übersetzen</mltext>
        <mltext language="NL">DeepL: Tekst vertalen</mltext>
        <mltext language="NV">DeepL: Translate text</mltext>
      </member>
    </object>
    <object class="TranslationItem" alias="TranslationItem_Übersetzung:Pleasemakesurethatproperties'DeepLAPIURL'and'DeepLAPIKey'areset.9">
      <member name="entryID">TranslationPleaseMakeSureThatPropertiesDeeplApiUrlAndDeeplApiKeyAreSet</member>
      <member name="text">
        <mltext language="DE">Bitte stellen Sie sicher, dass die Systemeinstellungen 'DeepL API URL' und 'DeepL API Key' gesetzt sind.</mltext>
        <mltext language="EN">Please make sure that system settings 'DeepL API URL' and 'DeepL API Key' are set.</mltext>
        <mltext language="FR">Assurez-vous que les paramètres du système 'DeepL API URL' et 'DeepL API Key' sont définis.</mltext>
        <mltext language="IT">Assicurarsi che le impostazioni di sistema 'DeepL API URL' e 'DeepL API Key' siano impostate.</mltext>
        <mltext language="DD">Bitte stellen Sie sicher, dass die Systemeinstellungen 'DeepL API URL' und 'DeepL API Key' gesetzt sind.</mltext>
        <mltext language="NL">Zorg ervoor dat de systeeminstellingen 'DeepL API URL' en 'DeepL API Key' zijn ingesteld.</mltext>
        <mltext language="NV">Please make sure that system settings 'DeepL API URL' and 'DeepL API Key' are set.</mltext>
      </member>
    </object>
    <object class="TranslationItem" alias="TranslationItem_Übersetzung:Textbeforetranslation8">
      <member name="entryID">TranslationTextBeforeTranslation</member>
      <member name="text">
        <mltext language="DE">Text vor der Übersetzung</mltext>
        <mltext language="EN">Text before translation</mltext>
        <mltext language="FR">Texte avant traduction</mltext>
        <mltext language="IT">Testo prima della traduzione</mltext>
        <mltext language="DD">Text vor der Übersetzung</mltext>
        <mltext language="NL">Tekst voor vertaling</mltext>
        <mltext language="NV">Text before translation</mltext>
      </member>
    </object>
  </objects>
  <settings />
</configset>