Wichtige Info

Die Inhalte, die du hier siehst stelle ich dir ohne Werbeanzeigen und ohne Tracking deiner Daten zur Verfügung. Trotzdem muss ich die Server bezahlen sowie Zeit in Recherche, Umsetzung sowie Mail Support stecken.
Um dies leisten zu können, verlinke ich in einigen Artikeln auf die Plattform Amazon. Alle diese Links nennen sich Afiliate Links. Wenn du dir mit diesem Link etwas kaufst, dann erhalte ich eine kleine Provision. Dies ändert jedoch NICHT den Preis, den du bezahlst!
Falls du mich also unterstützen möchtest, kannst du auf den Link zum Produkt klicken und hilfst mir dabei, dieses Hobby weiter zu betreiben.
Da ich Keine Werbung schalte und keine Spenden sammle, ist dies die einzige Möglichkeit, meine Systeme und mich zu finanzieren. Ich hoffe du kannst das verstehen :)



Python und LibreTranslate für multilinguale Inhalte verwenden


Einleitung

Dieser Beitrag gibt einen kleinen Einblick, wie dieser Blog und die Arbeit dahinter funktioniert.

Ich habe neben meinem Hauptjob kaum die Zeit Projekte durchzuführen, soziale Dinge zu machen und dann auch noch einen Blog so regelmäßig wie möglich zu pflegen. Aus diesem Grund versuche ich einige Dinge zu automatisieren. Seit längerer Zeit spiele ich mit der Idee, meine Inhalte auch in anderen Sprachen zur Verfügung zu stellen, um nicht nur die Reichweite ein wenig zu vergrößern, sondern gleichzeitig auch die Möglichkeit zu haben mehr Input zu bekommen.

Aus diesem Grund zeige ich in diesem Artikel, wie im Hintergrund meine Texte automatisch in andere Sprachen übersetzt werden.

In einem zukünftigen Artikel werde ich weiterhin zeigen, wie ich meine Texte auch voll automatisch auf Fehler korrigieren lasse, da auch das Korrekturlesen zeitaufwändig ist und gerade als Autor eines Textes schnell mal kleine Fehler übersehen werden.

Hauptteil

Erste Schritte

Wie so oft, wenn es Code bedarf, setze ich auf Python. Python kann ich schnell einbinden und ist eine wunderbare Sprache, wenn es um multimodale Inhalte aus verschiedenen Quellen geht. Zu Anfang hatte ich die Idee ChatGPT zu verwenden, jedoch hat sich nach den ersten Versionen herausgestellt, dass das nicht so ordentlich funktioniert. Man merkt am Ende des Tages, dass ChatGPT nicht wirklich versteht, was man möchte und dementsprechend schlecht zum Übersetzen geeignet ist. Es gab oftmals Probleme, mit gleichförmigen Outputs und gelegentlich wurde die Bedeutung von Sätzen schlichtweg geändert. Nach einigem Probieren und auch Anpassen von Werten, hatte ich mich dann gegen ChatGPT entschieden, was am Ende 2 Vorteile hat, wie sich herausstellt, denn aufgrund meiner finalen Lösung, muss ich 1. nichts für die Übersetzung bezahlen (abgesehen von den eigenen Betreiberkosten) und 2. kommt meine neue Lösung sehr flexibel daher und kann unter anderem auch HTML Inhalte nativ übersetzen.

Finale Version

Nachdem ChatGPT nicht funktioniert hat, wie ich es brauchte, habe ich das Projekt mangels ordentlicher Alternativen vorerst pausiert. Im November bin ich wegen meiner Arbeit auf ein Projekt gestoßen, welches sich LibreTranslate nennt. Dies ist ein (auch) lokales LLM, welches verwendet werden kann, um Texte zu übersetzen. Dabei kann man sowohl Dateien als auch Textauszüge übergeben und erhält diese übersetzt zurück (zzgl. einiger Metadaten).

LibreTranslate hat mich vom erstem Moment überzeugt. Es hat in einigen Sprachen Lücken, im Rahmen meiner Arbeit mussten mehrere hundert Flächendokumente (Arbeitsanweisungen) in 8 Sprachen übersetzt werden, um die Einarbeitung von neuen Personen zu erleichtern. Dabei habe ich bereits einiges mit LibreTranslate anstellen können und mir auch für mein Projekt einiges mitgenommen. Aufgrund der API lässt sich LibreTranslate sehr dynamisch einsetzen und es gibt für einige Programmiersprachen sogar Libaries, unter anderem für Python. Diese habe ich am Ende nicht verwendet bzw. für meine Ansprüche verbessert/modifiziert, da einige Einstellungen nicht möglich waren, welche ich aber benötigt habe, da man zum Beispiel das Eingabeformat nicht angeben konnte (Text oder HTML) und gerade dieser Parameter extrem viel Arbeit eingespart hat.

Wie funktioniert's?

Den Source Code selber, werde ich hier nicht posten, da das Programm noch nicht Battle-Tested ist und ich gerne noch einige Fehler ausmerzen möchte sowie einen Daemon für Unix Systeme einbauen, sodass die Übersetzung vollautomatisch stattfinden kann. Um die Funktion zu erörtern, habe ich jedoch ein vereinfachtes Ablaufdiagramm gebaut, welches die Übersetzung zusammenfasst.

Aufgrund des Umfangs, fehlen essenzielle Bestandteile im Programmablauf, zum Beispiel das Einlesen der Konfiguration. Das Programm ist in der Lage mithilfe der Konfiguration unterschiedliche Blogstrukturen zu übersetzen, sodass nicht nur dieser Blog, sondern theoretisch mehrere Blogs mit dem CMS und der API übersetzt werden könnten. In meinem Beispiel sieht die Config z.B. folgendermaßen aus:

{
    "translator_api": {
        "target_url": "http://translate.jr.local"
    },
    "cms_api": {
        "target": "https://pure-smart.de",
        "endpoint": "api/pages",
        "page": "blog",
        "limit": 1,
        "api_user_needed": true,
        "api_username": "<<my_user>>",
        "api_password": "<<my_pass>>"   
    },    
    "log_level": "Warning",
    "target_lang": [
        {
            "name": "english",
            "lang_code": "en",
            "fields": {
                "check_marker": "transl-enabled",
                "success_marker": "english-translated"
            }
        }
    ],
    "limit": 3,
    "rules": [
        {
            "wildcard_fields": false,
            "allowed_fields": ["cover", "content", "summary", "main_title", "gt", "descr", "m_tit", "og_tit", "og_descr"],
            "exceptions": [],
            "page_name": "blog",
            "fields": [
                {
                    "field_name": "cover",
                    "translate": false
                },
                {
                    "field_name": "main_title",
                    "translate": "raw"
                },
                {
                    "field_name": "caption",
                    "translate": "raw"
                },
                {
                    "field_name": "p_caption",
                    "translate": "raw"
                },
                {
                    "field_name": "no_posts",
                    "translate": "raw"
                },
                {
                    "field_name": "pinned_warn",
                    "translate": "raw"
                },
                {
                    "field_name": "summary",
                    "translate": "raw"
                },
                {
                    "field_name": "descr",
                    "translate": "raw"
                },
                {
                    "field_name": "m_tit",
                    "translate": "raw"
                },
                {
                    "field_name": "og_tit",
                    "translate": "raw"
                },
                {
                    "field_name": "og_descr",
                    "translate": "raw"
                },
                {
                    "field_name": "gt",
                    "translate": "raw"
                },
                {
                    "rule_name": "content-Gallery_Block",
                    "field_name": "content",
                    "translate": "json-array", 
                    "content_key": "content.caption",
                    "filter": [
                        {
                            "key": "type",
                            "value": "gallery"
                        }
                    ]
                },
                {
                    "rule_name": "content-Heading_Block",
                    "field_name": "content",
                    "translate": "json-array", 
                    "content_key": "content.text",
                    "filter": [
                        {
                            "key": "type",
                            "value": "heading"
                        }
                    ]
                },
                {
                    "rule_name": "content-Image_Block",
                    "field_name": "content",
                    "translate": "json-array", 
                    "content_key": "content.alt",
                    "filter": [
                        {
                            "key": "type",
                            "value": "image"
                        }
                    ]
                },
                {
                    "rule_name": "content-Image_Block_2",
                    "field_name": "content",
                    "translate": "json-array", 
                    "content_key": "content.caption",
                    "filter": [
                        {
                            "key": "type",
                            "value": "image"
                        }
                    ]
                },
                {
                    "rule_name": "content-list_block",
                    "field_name": "content",
                    "translate": "json-array", 
                    "content_key": "content.text",
                    "filter": [
                        {
                            "key": "type",
                            "value": "list"
                        }
                    ]
                },
                {
                    "rule_name": "content-Quote_Block",
                    "field_name": "content",
                    "translate": "json-array", 
                    "content_key": "content.text",
                    "filter": [
                        {
                            "key": "type",
                            "value": "quote"
                        }
                    ]
                },
                {
                    "rule_name": "content-Quote_Block",
                    "field_name": "content",
                    "translate": "json-array", 
                    "content_key": "content.citation",
                    "filter": [
                        {
                            "key": "type",
                            "value": "quote"
                        }
                    ]
                },
                {
                    "rule_name": "content-Text_Block",
                    "field_name": "content",
                    "translate": "json-array", 
                    "content_key": "content.text",
                    "translation_type": "html",
                    "filter": [
                        {
                            "key": "type",
                            "value": "text"
                        }
                    ]
                },
                {
                    "rule_name": "content-Video_Block",
                    "field_name": "content",
                    "translate": "json-array", 
                    "content_key": "content.caption",
                    "filter": [
                        {
                            "key": "type",
                            "value": "video"
                        }
                    ]
                },
                {
                    "rule_name": "content-Code_Block",
                    "field_name": "content",
                    "translate": false, 
                    "content_key": "content.code",
                    "filter": [
                        {
                            "key": "type",
                            "value": "code"
                        }
                    ]
                }
            ]

        }
    ]
}

Mithilfe der Konfigurationsdatei habe ich die Möglichkeit, das Programm dynamisch an den Blog anzupassen und jederzeit Änderungen durchzuführen.

Zuletzt ist im folgenden noch ein vereinfachtes Ablaufdiagramm des Programms abgebildet. Es fasst den Ablauf sehr vereinfacht zusammen. Es fehlen Dinge wie die Authentifizierung gegenüber den Systemen sowie die Verarbeitung der Blog-Posts. Man bekommt jedoch at least eine Idee, wie dieser Blog übersetzt wird :)


Back…