Massenübersetzung von Sprachdateien in XML formatiert

peterle

Forenkasper
Wie es der Betrieb von Foren so mit sich bringt, braucht man da Sprachpakete und die kommen in XML verpackt, was z.B. so aussieht:

Code:
<?xml version="1.0" encoding="UTF-8"?>
<language>
 <app key="core" version="104012">
  <word key="__app_core" js="0">System</word>
  <word key="__indefart_personal_conversation" js="0">a personal conversation</word>
  <word key="__indefart_personal_conversation_message" js="0">a message in a personal conversation</word>
  <word key="__indefart_report" js="0">a report</word>
  <word key="__indefart_report_comment" js="0">a comment on a report</word>
  <word key="__indefart_status_reply" js="0">a reply to a status update</word>
  <word key="__indefart_member_status" js="0">a status update</word>
  <word key="__defart_personal_conversation" js="0">personal conversation</word>
  <word key="__defart_personal_conversation_message" js="0">message in a personal conversation</word>
  <word key="__defart_report" js="0">report</word>
...

Nun hat ja niemand Lust 22k Zeilen davon zu lesen und händisch zu übersetzen, also war die Frage, wie mache ich das maschinell.

Es gibt:
und vermutlich noch einige andere.

Mit "der Gogel" sind die Ergebnisse dem Anschein nach hinreichen gut, wenn man das XML per Copy&Paste ins Fenster schmeißt, aber er zerschießt einem die Syntax. Da fehlen nachher Abschlußtags, teilweise übersetzt er Tags, teilweise nicht. Einige Zeichen wirbelt er auch gerne durcheinander oder wirft sie einfach weg, auch hat er irgendwo eine Kiste Leerzeichen stehen, die er dringend in den Text werfen muß, was das Ergebnis nur aufwendig reparierbar macht.
Man kann auch nur 5000 Zeichen am Stück übersetzen lassen, was eine große Kopierorgie wird, bei der man sich tunlichst nicht vertut.

Apertium ist sehr charmant, kann aber Online nur romanische Sprachen. Man findet aber ein Sprachdatei für englisch/deutsch in der Installation. Die ist auch recht flott gemacht, wenn man sich nicht an irgendwelche Paketanbieter hält, sondern sich das Ding selber baut. Er macht das mit einem Installer zumindest auf Linux sehr elegant und fischt sich auch die gewünschten Sprachen bei Bedarf zusammen und installiert sie.
Erfreulicherweise kann er WXML und zwar vorwärts und rückwärts. Er macht sich quasi das XML mit Klammern so, daß er nur noch den Inhalt, aber nicht die Syntax übersetzt und nachher baut er das wieder zum XML zusammen.
Das ist eine feine Sache, aber die Übersetzung liest sich, wie ein Lehgasteniker auf Drogen ... kann man also so nicht brauchen.
(normale Texte muß ich mal probieren)

DeepL ist ein Aboanbieter und die Ergebnisse auf der Webseite per Copy&Paste zerschießen einem zumindest nicht mehr die Syntax, die Übersetzung ist auch gut, aber wenn man ihm ein 1MB XML füttert, dann kommen nachher doch einige Syntaxfehler zusammen. Vor allem fummelt er einem wie Google auch irgendwelche ESC-Sequenzen oder sonstwas unsichtbares rein, was ich da aktuell nur händisch mit Gefrickel wieder rauskriege. Ist aber besser als Google, da er nicht tausende Leerzeichen einfügt.

Generell problematisch sind die Phrasen, in denen nicht nur Text, sondern auch Code steht, wie diese hier z.B.

Code:
<word key="rss_feed_blurb" js="0"><![CDATA[RSS Feeds allow users to use an RSS Reader to see new content on your community. You can set up different RSS Feeds to cover different areas of your community. <a href='{internal.app=core&module=discovery&controller=streams}'>Activity Streams</a> automatically provide their own RSS Feed.]]></word>

<word key="_date_this_week_c" js="0">{!0#[0:Sunday][1:Monday][2:Tuesday][3:Wednesday][4:Thursday][5:Friday][6:Saturday]} at %s</word>

  <word key="emoji_style_twemoji" js="0"><![CDATA[Twitter Style<br><div class='ipsType_large ipsSpacer_top ipsSpacer_half'><img src='https://twemoji.maxcdn.com/2/72x72/1f600.png' class='ipsEmoji'> <img src='https://twemoji.maxcdn.com/2/72x72/1f609.png' class='ipsEmoji'> <img src='https://twemoji.maxcdn.com/2/72x72/1f602.png' class='ipsEmoji'> <img src='https://twemoji.maxcdn.com/2/72x72/1f60d.png' class='ipsEmoji'> <img src='https://twemoji.maxcdn.com/2/72x72/1f918.png' class='ipsEmoji'> <img src='https://twemoji.maxcdn.com/2/72x72/1f926-200d-2640-fe0f.png' class='ipsEmoji'> <img src='https://twemoji.maxcdn.com/2/72x72/1f937-200d-2642-fe0f.png' class='ipsEmoji'> <img src='https://twemoji.maxcdn.com/2/72x72/1f37f.png' class='ipsEmoji'> <img src='https://twemoji.maxcdn.com/2/72x72/1f680.png' class='ipsEmoji'> <img src='https://twemoji.maxcdn.com/2/72x72/1f389.png' class='ipsEmoji'> <img src='https://twemoji.maxcdn.com/2/72x72/1f3f3-fe0f-200d-1f308.png' class='ipsEmoji'></div>]]></word>

Wirklich interessant wäre es, wenn es auf einfache Art gelingen würde, entweder das XML in eine Liste zu verwandeln und nachher wieder zurück in eine, die man sich dann zerlegen und filtern könnte, um nur noch übersetzbare Phrasen als wie auch immer geartete Liste zu haben, die man dann übersetzen kann und dann wieder ein XML daraus bauen lassen kann.

XML Copy Editor und Vim haben sich übrigens beim bearbeiten bewährt.

Zur Info, aber wer Ideen hat, dem höre ich wie immer gerne zu. Die Übersetzungsprogramme sind mittlerweile so gut, daß der Weg endlich gangbar scheint.
 
Gut. XML zu parsen und dann die entsprechenden Wortgruppen nach Google-Translate zu schicken und die Antwort wieder ins XML einzutragen ist ansich ja nu kein großes Problem.
Woran scheitert es denn genau?
 
Das klingt schon nach Luxus ... :rolleyes:

Schubs mich doch bitte mal in die richtige Richtung. Aktuell sehe ich den Wald vor lauter Bäumen nicht, das ist so alles gar nicht meine Baustelle.
 
Das hier ist zumindest ein Anfang ....
Code:
xml2 < core-lang.xml | grep word= | trans -b

allerdings kickt mich scheinbar google nach ein paar Anfragen raus und es kommt nur noch ein leere Antwort.
 
Ja, Google Translate ist ein kostenpflichtiger Service. Kostenlos sind nur eine handvoll Requests pro Tag.
 
Gute Frage. trans wird ja über die API gehen. Die ist definitiv begrenzt, dass weiß ich sicher. Wenn man größere Mengen übersetzen will, muss man sich einen API-Key kaufen und entsprechen bezahlen. Bei der Website könnte ich mir vorstellen, dass sie unbegrenzt ist, da man dort sowieso keine größeren Mengen Request mit generiert bekommt.
 
Kommunikation ist ja nicht googles Stärke ... viele bunte Bilder, tolle Teaser, wenig Inhalt.

20$ pro 1M Zeichen und es sind summasummarum 1.6M Zeichen ... das ist ja alles im bezahlbaren Rahmen. Interessant.
 
Schubs mich doch bitte mal in die richtige Richtung.
Naja. Ich würde es mit Racket (einem Lisp/Scheme Derivat) lösen. Aber in welcher Sprache Du es löst, ist eigentlich irrelevant, da das Handwerkszeug was man braucht bei "allen" da ist.

Als erstes ein bisschen Konfigurationszeug. Konstanten für Eingabedatei, Ausgabedatei:
Code:
(define INPUTFILE "source.xml")
(define OUTPUTFILE "dest.xml")

Dann das XML-Parsing. In Racket ist dafür das Standardmodul xml.
(require xml)

XML-Parsing wird damit zum Kinderspiel:
Code:
(define src-xml-doc (with-input-from-file
                    INPUTFILE
                  (lambda()
                    (read-xml))))
Allerdings würde ich dann nicht auf XML-Strukturen arbeiten, sondern mit Lisp-Listen. Das geht dann natürlicher von der Hand, weil XML ja quasi nichts anderes als verschachtelte Listen sind.
Mit Racket-Sprech sind das X-Expr:
Code:
(xml->xexpr (document-element src-xml-doc ))

Die Verarbeitung ist dann fast schon das "anstrengenste" von der Codelänge her gesehen. Darum hab ich mal bei mir zuhause in den Code-Schrank gegriffen. XML-Bearbeitung mache ich ja auch nicht zum ersten mal.
Das dann noch ein wenig angepasst und wir erhalten folgende Funktion:
Code:
(define (convert-xexpr xexpr)

  (define (process-childs childs [condition (lambda(el) #t)] [action (lambda(el) el)])
    (for/list ([el childs]
               #:when (not (and (string? el) (= (string-length (string-trim el)) 0))))
      (cond
        [(list? el)      (process-xml-tag el)]
        [(condition el)  (action el)]
        [else            el]
        )))
 
  (define (process-xml-tag xexpr)
    (let ([tagname (first xexpr)]
          [attr    (second xexpr)]
          [childs  (cdr (cdr xexpr))])
      (cons tagname
            (cons attr
                  (if (string=? (string-downcase (symbol->string tagname)) "word")
                      (process-childs childs (lambda(el) (string? el)) (lambda(el) (translate-sentence el)))
                      (process-childs childs)
                      )))))
  (process-xml-tag xexpr)
  )

Die eigentliche Übersetzung geschieht dann durch die Funktion translate-sentence
die wir aber noch nicht definiert haben, weil wir noch nicht wissen wie der Google-Kram funktioniert.

Also machen wir https://translate.google.de/ im Browser auf und gucken mit den Developertools, was wie wohin geschickt werden muss.
Es stellt sich raus, die Übersetzung findet via Javascript statt was ein JSON zurückgibt.
Die Request-URL sieht z.B. so aus:
https://translate.google.de/translate_a/single?client=webapp&sl=en&tl=de&hl=de&dt=at&dt=bd&dt=ex&dt=ld&dt=md&dt=qca&dt=rw&dt=rm&dt=ss&dt=t&otf=2&ssel=3&tsel=0&kc=4&tk=705839.807319&q=a%20report

Der letzte URL-Paraleter q gibt die zu suchende Wortgruppe an.
Vermutlich ist es auch schlau irgendwie die Cookies aus dem Browser zu übernehmen usw., damit Google "denkt" wir sind mit unseren Abfragen noch in der Browser-Session.

Code:
(define (translate-sentence str)
  (define (http-request url)

  (call/input-request "1.1"
                      "GET"
                      url
                      #hash(
                            ("Host" . "translate.google.de")
                            ("User-Agent" . "Mozilla/5.0")
                            ("Accept" . "*/*")
                            ("Referer" .  "https://translate.google.de/")
                            ("Cookie" . "1P_JAR=2019-3-15-8; NID=164=ZUa1llXWUPxn689JDlhCXJPcF4oircebJKqMAVyHtGAby3D-wggX99yRkyOJyTy1lnc5N7hmZ0FpD6l13UF4E7xWjEH7nK67vljGyk04NCY371SWY5XPuXrVBiDb-GIjywrhjNKkUP_GN7YnvRvRqFxO35T6sngXxkgyRDtSlAI; CONSENT=WP.277bf6")
                            )
                      read-entity/bytes
                      #:redirects 2))
  (define google-translate-url "https://translate.google.de/translate_a/single?client=webapp&sl=en&tl=de&hl=de&dt=at&dt=bd&dt=ex&dt=ld&dt=md&dt=qca&dt=rw&dt=rm&dt=ss&dt=t&otf=2&ssel=3&tsel=0&kc=4&tk=705839.807319&q=~a")
; Quick&Dirty-parse of JSON-Result
  (list-ref (string-split
             (bytes->string/utf-8 (http-request (format google-translate-url (uri-encode str)))))
             1)
  )

Zusammengerühert und vervollständigt sieht dann der Racket-Code so aus:
Code:
#lang racket

(define INPUTFILE "source.xml")
(define OUTPUTFILE "dest.xml")

(require xml
         net/uri-codec
         http/request)



(define (translate-sentence str)
  (define (http-request url)

  (call/input-request "1.1"
                      "GET"
                      url
                      #hash(
                            ("Host" . "translate.google.de")
                            ("User-Agent" . "Mozilla/5.0")
                            ("Accept" . "*/*")
                            ("Referer" .  "https://translate.google.de/")
                            ("Cookie" . "1P_JAR=2019-3-15-8; NID=164=ZUa1llXWUPxn689JDlhCXJPcF4oircebJKqMAVyHtGAby3D-wggX99yRkyOJyTy1lnc5N7hmZ0FpD6l13UF4E7xWjEH7nK67vljGyk04NCY371SWY5XPuXrVBiDb-GIjywrhjNKkUP_GN7YnvRvRqFxO35T6sngXxkgyRDtSlAI; CONSENT=WP.277bf6")
                            )
                      read-entity/bytes
                      #:redirects 2))
  (define google-translate-url "https://translate.google.de/translate_a/single?client=webapp&sl=en&tl=de&hl=de&dt=at&dt=bd&dt=ex&dt=ld&dt=md&dt=qca&dt=rw&dt=rm&dt=ss&dt=t&otf=2&ssel=3&tsel=0&kc=4&tk=705839.807319&q=~a")
  (list-ref (string-split
             (bytes->string/utf-8 (http-request (format google-translate-url (uri-encode str)))))
             1)
  )

(define (convert-xexpr xexpr)

  (define (process-childs childs [condition (lambda(el) #t)] [action (lambda(el) el)])
    (for/list ([el childs]
               #:when (not (and (string? el) (= (string-length (string-trim el)) 0))))
      (cond
        [(list? el)      (process-xml-tag el)]
        [(condition el)  (action el)]
        [else            el]
        )))
 
  (define (process-xml-tag xexpr)
    (let ([tagname (first xexpr)]
          [attr    (second xexpr)]
          [childs  (cdr (cdr xexpr))])
      (cons tagname
            (cons attr
                  (if (string=? (string-downcase (symbol->string tagname)) "word")
                      (process-childs childs (lambda(el) (string? el)) (lambda(el) (translate-sentence el)))
                      (process-childs childs)
                      )))))
  (process-xml-tag xexpr)
  )


; Parse XML File
(define src-xml-doc (with-input-from-file
                    INPUTFILE
                  (lambda()
                    (read-xml))))

; Convert/Translate
(define result-element (convert-xexpr (xml->xexpr
               (document-element src-xml-doc ))))

; Write out Result
(with-output-to-file
    OUTPUTFILE
  (lambda()
    (display-xml
     (make-document
      (document-prolog src-xml-doc)
      (xexpr->xml result-element)
      (document-misc src-xml-doc)))))

Wenn man da jetzt z.B. mit Deinem Beispiel ran geht und
XML:
<?xml version="1.0" encoding="UTF-8"?>
<language>
 <app key="core" version="104012">
  <word key="__app_core" js="0">System</word>
  <word key="__indefart_personal_conversation" js="0">a personal conversation</word>
  <word key="__indefart_personal_conversation_message" js="0">a message in a personal conversation</word>
  <word key="__indefart_report" js="0">a report</word>
 </app>
</language>
reinwirft kommt
XML:
<?xml version="1.0" encoding="UTF-8"?>

<language>
  <app key="core" version="104012">
    <word js="0" key="__app_core">
      System
    </word>
    <word js="0" key="__indefart_personal_conversation">
      ein persönliches Gespräch
    </word>
    <word js="0" key="__indefart_personal_conversation_message">
      eine Nachricht in einem persönlichen Gespräch
    </word>
    <word js="0" key="__indefart_report">
      ein Bericht
    </word>
  </app>
</language>
wieder raus.

Funktioniert etwas, aber noch nicht wirklich gut. Kommt recht schnell kein JSON mehr zurück, sondern HTML mit einer Meldung. Möglicherweise wird das Cookie angepasst und das stimmt dann beim Request nicht mehr. Da muss man noch mal etwas nachforschen.
Zwischen den Request sollte man vielleicht auch ein Random-Sleep zwischenfügen, damit die Requests nicht zu schnell hintereinander kommen.

das ist ja alles im bezahlbaren Rahmen. Interessant.
Wenn man die API abfragen kann, dann macht es das natürlich einfacher und zuverlässiger. Im Augenblick ist das ja eher so ein Web-"Hack".
 
Zurück
Oben