{"id":7370,"date":"2025-04-21T07:02:55","date_gmt":"2025-04-21T07:02:55","guid":{"rendered":"https:\/\/textsnapper.com\/?page_id=7370"},"modified":"2025-07-23T09:13:35","modified_gmt":"2025-07-23T09:13:35","slug":"translate","status":"publish","type":"page","link":"https:\/\/textsnapper.com\/en\/translate\/","title":{"rendered":"translate"},"content":{"rendered":"<div data-elementor-type=\"wp-page\" data-elementor-id=\"7370\" class=\"elementor elementor-7370\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-d2d06d5 e-flex e-con-boxed e-con e-parent\" data-id=\"d2d06d5\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-93e4d0f elementor-widget elementor-widget-html\" data-id=\"93e4d0f\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!DOCTYPE html>\r\n<html lang=\"de\">\r\n<head>\r\n  <meta charset=\"UTF-8\">\r\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n  <title>OCR-Texterkennung & \u00dcbersetzung<\/title>\r\n  <style>\r\n    #ocr-widget {\r\n      border: 1px solid #ccc; \r\n      font-family: Arial, sans-serif;\r\n      text-align: center;\r\n      background-color: #f9f9f9;\r\n      border-radius: 10px;\r\n      padding: 20px;\r\n      max-width: 700px;\r\n      margin: 40px auto;\r\n    }\r\n\r\n    .main-btn, .btn {\r\n      padding: 10px 20px;\r\n      background: #2F5591;\r\n      color: #FFF;\r\n      font-weight: bold;\r\n      border: none;\r\n      border-radius: 4px;\r\n      cursor: pointer;\r\n      margin: 5px;\r\n      transition: background 0.3s, transform 0.2s ease;\r\n    }\r\n    .main-btn:hover, .btn:hover {\r\n      background: #1d3f6c;\r\n      transform: scale(1.02);\r\n    }\r\n\r\n    .dropdown {\r\n      padding: 8px;\r\n      font-size: 16px;\r\n      margin: 5px;\r\n      border-radius: 4px;\r\n      max-width: 250px;\r\n    }\r\n\r\n    #ocr-widget img, #ocr-widget video {\r\n      width: 100%;\r\n      max-width: 600px;\r\n      border-radius: 8px;\r\n      margin-top: 15px;\r\n    }\r\n\r\n    #ocr-result, #translation-result, #detected-lang {\r\n      padding: 10px;\r\n      margin-top: 15px;\r\n      border-radius: 8px;\r\n      border: 1px solid #ccc;\r\n      background: #fff;\r\n      min-height: 50px;\r\n      text-align: left;\r\n    }\r\n\r\n    .translated-block {\r\n      background: #f0f8ff;\r\n      padding: 10px;\r\n      margin-bottom: 10px;\r\n      border-radius: 6px;\r\n      border: 1px solid #d0e0f0;\r\n      line-height: 1.6;\r\n    }\r\n\r\n    .highlight { background-color: yellow; }\r\n\r\n    \/* Fullscreen Overlay *\/\r\n    .fullscreen-overlay {\r\n      position: fixed; top: 0; left: 0;\r\n      width: 100%; height: 100%;\r\n      background: #fff; z-index: 9999;\r\n      display: none; flex-direction: column;\r\n    }\r\n    .fullscreen-header, .fullscreen-footer {\r\n      padding: 15px; background: #fff; border-bottom: 1px solid #ddd;\r\n    }\r\n    .fullscreen-header { font-size: 20px; font-weight: bold; cursor: pointer; }\r\n    .fullscreen-content {\r\n      flex: 1; overflow-y: auto; padding: 20px;\r\n      font-size: 1.3em; line-height: 1.6; color: #333;\r\n    }\r\n    .fullscreen-footer {\r\n      border-top: 1px solid #ddd; text-align: center;\r\n    }\r\n    .font-controls {\r\n      display: flex; align-items: center; justify-content: center;\r\n      gap: 10px; margin-top: 10px;\r\n    }\r\n    .font-controls button { padding: 8px 12px; }\r\n    .font-controls input[type=\"range\"] { width: 50%; }\r\n    #cols-wrap h3 {\r\n      margin-top: 1em;\r\n      font-size: 1.1em;\r\n      border-bottom: 1px solid #aaa;\r\n    }\r\n    #cols-wrap p {\r\n      margin: 0.3em 0;\r\n    }\r\n    #structured-output h3 {\r\n      margin-top: 0.8em;\r\n      font-size: 1.1em;\r\n      border-bottom: 1px solid #ccc;\r\n    }\r\n    #structured-output p {\r\n      margin: 0.3em 0;\r\n      line-height: 1.4;\r\n    }\r\n  <\/style>\r\n<\/head>\r\n<body>\r\n<div id=\"translations\" style=\"display:none;\">\r\n  <span id=\"title-de\">\ud83d\udcf7 Translate text on images \ud83c\udf10<\/span>\r\n  <span id=\"title-en\">\ud83d\udcf7 Translate text from images \ud83c\udf10<\/span>\r\n\r\n  <span id=\"photo-de\">\ud83d\udcf8 Take a photo<\/span>\r\n  <span id=\"photo-en\">\ud83d\udcf8 Take photo<\/span>\r\n\r\n  <span id=\"upload-de\">\ud83d\udcc1 Upload image<\/span>\r\n  <span id=\"upload-en\">\ud83d\udcc1 Upload image<\/span>\r\n\r\n  <span id=\"target-lang-label-de\">\ud83c\udf0d Target language:<\/span>\r\n  <span id=\"target-lang-label-en\">\ud83c\udf0d Target language:<\/span>\r\n\r\n  <span id=\"translate-de\">\ud83c\udf10 \u00dcbersetzen<\/span>\r\n  <span id=\"translate-en\">\ud83c\udf10 Translate<\/span>\r\n\r\n  <span id=\"translating-de\">\u23f3 \u00dcbersetze...<\/span>\r\n  <span id=\"translating-en\">\u23f3 Translating...<\/span>\r\n\r\n  <span id=\"read-aloud-de\">\ud83d\udd0a Read aloud<\/span>\r\n  <span id=\"read-aloud-en\">\ud83d\udd0a Read aloud<\/span>\r\n\r\n  <span id=\"back-de\">\u2190 Back<\/span>\r\n  <span id=\"back-en\">\u2190 Back<\/span>\r\n\r\n  <span id=\"detected-prefix-de\">Erkannte Sprache:<\/span>\r\n  <span id=\"detected-prefix-en\">Detected language:<\/span>\r\n\r\n  <span id=\"dialect-label-de\">Dialect:<\/span>\r\n  <span id=\"dialect-label-en\">Dialect:<\/span>\r\n\r\n  <span id=\"gender-label-de\">Gender:<\/span>\r\n  <span id=\"gender-label-en\">Gender:<\/span>\r\n\r\n  <span id=\"quality-label-de\">Quality:<\/span>\r\n  <span id=\"quality-label-en\">Quality:<\/span>\r\n\r\n  <span id=\"voice-label-de\">Voice:<\/span>\r\n  <span id=\"voice-label-en\">Voice:<\/span>\r\n\r\n  <span id=\"speed-label-de\">Speed:<\/span>\r\n  <span id=\"speed-label-en\">Speed:<\/span>\r\n\r\n  <span id=\"gender-option-0-de\">Beliebig<\/span>\r\n  <span id=\"gender-option-0-en\">Any<\/span>\r\n  <span id=\"gender-option-1-de\">Masculine<\/span>\r\n  <span id=\"gender-option-1-en\">Male<\/span>\r\n  <span id=\"gender-option-2-de\">Female<\/span>\r\n  <span id=\"gender-option-2-en\">Female<\/span>\r\n\r\n  <span id=\"speed-option-0-de\">Slow<\/span>\r\n  <span id=\"speed-option-0-en\">Slow<\/span>\r\n  <span id=\"speed-option-1-de\">Normal<\/span>\r\n  <span id=\"speed-option-1-en\">Normal<\/span>\r\n  <span id=\"speed-option-2-de\">Fast<\/span>\r\n  <span id=\"speed-option-2-en\">Fast<\/span>\r\n  <span id=\"speed-option-3-de\">Very fast<\/span>\r\n  <span id=\"speed-option-3-en\">Very fast<\/span>\r\n<\/div>\r\n\r\n<div id=\"ocr-widget\">\r\n  <h2>\ud83d\udcf7 Translate text on images \ud83c\udf10<\/h2>\r\n\r\n  <button id=\"photo-btn\" class=\"main-btn\">\ud83d\udcf8 Take a photo<\/button>\r\n  <button id=\"upload-btn\" class=\"main-btn\">\ud83d\udcc1 Upload image<\/button>\r\n  <input type=\"file\" id=\"file-input\" accept=\"image\/*\" capture=\"environment\" hidden>\r\n\r\n  <video id=\"camera\" autoplay playsinline style=\"display:none;\"><\/video>\r\n  <img id=\"preview-img\" style=\"display:none;\">\r\n\r\n  \r\n  <div id=\"detected-lang\" style=\"display:none;\"><\/div>\r\n\r\n\r\n  <div class=\"translation-controls\">\r\n    <label for=\"target-lang\">\ud83c\udf0d Target language:<\/label>\r\n    <select id=\"target-lang\" class=\"dropdown\"><\/select>\r\n    <button id=\"translate-btn\" class=\"main-btn\">\ud83c\udf10 \u00dcbersetzen<\/button>\r\n  <\/div>\r\n\r\n  <button id=\"tts-btn\" class=\"main-btn\" style=\"display:none;\">\ud83d\udd0a Read aloud<\/button>\r\n\r\n  <div id=\"voice-settings\" style=\"display:none; margin-top:20px;\">\r\n  <div style=\"margin-bottom: 10px;\">\r\n    <label for=\"dialectSelect\" style=\"display:block; margin-bottom: 4px; font-weight:bold;\">Dialect:<\/label>\r\n    <select id=\"dialectSelect\" class=\"dropdown\"><\/select>\r\n  <\/div>\r\n\r\n  <div style=\"margin-bottom: 10px;\">\r\n    <label for=\"genderSelect\" style=\"display:block; margin-bottom: 4px; font-weight:bold;\">Gender:<\/label>\r\n    <select id=\"genderSelect\" class=\"dropdown\"><\/select>\r\n  <\/div>\r\n\r\n  <div style=\"margin-bottom: 10px;\">\r\n    <label for=\"qualitySelect\" style=\"display:block; margin-bottom: 4px; font-weight:bold;\">Quality:<\/label>\r\n    <select id=\"qualitySelect\" class=\"dropdown\"><\/select>\r\n  <\/div>\r\n\r\n  <div style=\"margin-bottom: 10px;\">\r\n    <label for=\"voiceSelect\" style=\"display:block; margin-bottom: 4px; font-weight:bold;\">Voice:<\/label>\r\n    <select id=\"voiceSelect\" class=\"dropdown\"><\/select>\r\n  <\/div>\r\n\r\n  <div style=\"margin-bottom: 10px;\">\r\n    <label for=\"speedSelect\" style=\"display:block; margin-bottom: 4px; font-weight:bold;\">Speed:<\/label>\r\n    <select id=\"speedSelect\" class=\"dropdown\">\r\n      <option value=\"0.5\">Slow<\/option>\r\n      <option value=\"1\" selected>Normal<\/option>\r\n      <option value=\"1.5\">Fast<\/option>\r\n      <option value=\"2\">Very fast<\/option>\r\n    <\/select>\r\n  <\/div>\r\n  <\/div>\r\n  <div id=\"structured-output\" style=\"display: flex; gap:20px; margin-top:20px;\"><\/div>\r\n<\/div>\r\n\r\n<div id=\"fullscreenTTS\" class=\"fullscreen-overlay\">\r\n  <div class=\"fullscreen-header\" id=\"closeFullscreen\">\u2190 Back<\/div>\r\n  <div class=\"fullscreen-content\" id=\"fullscreenText\"><\/div>\r\n  <div class=\"fullscreen-footer\">\r\n    <button id=\"fullscreenPlay\" class=\"btn\">\ud83d\udd0a Read aloud<\/button>\r\n    <div class=\"font-controls\">\r\n      <button id=\"fontSmaller\" class=\"btn\">A-<\/button>\r\n      <input type=\"range\" id=\"fontSizeSlider\" min=\"16\" max=\"80\" value=\"32\">\r\n      <button id=\"fontLarger\" class=\"btn\">A+<\/button>\r\n    <\/div>\r\n  <\/div>\r\n<\/div>\r\n\r\n<script>\r\n\r\nconst lang = window.location.pathname.startsWith(\"\/en\/\")\r\n             ? \"en\"\r\n             : document.documentElement.lang.startsWith(\"en\")\r\n               ? \"en\"\r\n               : \"de\";\r\n               \r\nconst G = {\r\n  en: { any: \"Any\",   male: \"Male\",   female: \"Female\" },\r\n  de: { any: \"Beliebig\", male: \"M\u00e4nnlich\", female: \"Weiblich\" }\r\n}[lang];\r\n\r\n\/\/ COMBINED getText function - handles both ID-based and key-based translations\r\nconst getText = (key) => {\r\n  \/\/ First check if it's an ID-based translation (like \"photo-de\", \"translate-en\")\r\n  const el = document.getElementById(`${key}-${lang}`);\r\n  if (el) {\r\n    return el.textContent;\r\n  }\r\n  \r\n  \/\/ Fallback to hardcoded translations for common keys\r\n  const translations = {\r\n    'translating': document.documentElement.lang.startsWith('en') ? 'Translating...' : '\u00dcbersetze...',\r\n    'translate': document.documentElement.lang.startsWith('en') ? 'Translate' : '\u00dcbersetzen',\r\n    'read-aloud': document.documentElement.lang.startsWith('en') ? 'Read aloud' : 'Vorlesen',\r\n    'detected-prefix': document.documentElement.lang.startsWith('en') ? 'Detected language:' : 'Erkannte Sprache:'\r\n  };\r\n  \r\n  return translations[key] || key;\r\n};\r\n\r\nlet fileInput, previewImg, langBox, translateBtn, ttsBtn, ajaxUrl;\r\nlet availableVoices = {};\r\nlet userVoiceSettings = window.userVoiceSettings || {};\r\nlet translatedText = '';\r\nlet ttsInterval;\r\nlet currentWord = 0;\r\n\r\n\/\/ Neuer, dynamischer Mapper f\u00fcr Sprachnamen\r\nconst languageNameDisplay = new Intl.DisplayNames(\r\n  [document.documentElement.lang.startsWith(\"en\") ? \"en\" : \"de\"],\r\n  { type: \"language\" }\r\n);\r\n\r\nasync function fetchUserVoices() {\r\n  try {\r\n    const res = await fetch(ajaxUrl, {\r\n      method: 'POST',\r\n      credentials: 'include',\r\n      body: new URLSearchParams({ action: 'my_get_user_voices' })\r\n    });\r\n    const j = await res.json();\r\n    return (j.success && j.data && j.data.voices) ? j.data.voices : {};\r\n  } catch (e) {\r\n    console.warn(\"Fehler beim Laden userVoiceSettings:\", e);\r\n    return {};\r\n  }\r\n}\r\n\r\n\/\/ Anwenden der gespeicherten Stimme f\u00fcr einen Dialekt\/Zielsprache\r\nfunction applyUserVoiceSettingsTrans(targetLang) {\r\n  \/\/ Ziel: z.B. \"en\", \"de-DE\", \"es-ES\"\r\n  let uset = userVoiceSettings[targetLang];\r\n  let usedDial = targetLang;\r\n\r\n  if (!uset) {\r\n    \/\/ Partial-Match, z.B. \"en\" f\u00fcr \"en-US\"\r\n    const allKeys = Object.keys(userVoiceSettings);\r\n    const partial = allKeys.find(k => k.toLowerCase().startsWith(targetLang.toLowerCase() + \"-\"));\r\n    if (partial) {\r\n      usedDial = partial;\r\n      uset = userVoiceSettings[partial];\r\n    }\r\n  }\r\n  if (!uset) return;\r\n\r\n  \/\/ Dropdowns holen\r\n  const dialectSelect = document.getElementById('dialectSelect');\r\n  const genderSelect = document.getElementById('genderSelect');\r\n  const qualitySelect = document.getElementById('qualitySelect');\r\n  const voiceSelect = document.getElementById('voiceSelect');\r\n\r\n  \/\/ Dialekt setzen\r\n  if (dialectSelect && usedDial) {\r\n    dialectSelect.value = usedDial;\r\n    dialectSelect.dispatchEvent(new Event('change')); \/\/ Triggert Neuaufbau!\r\n  }\r\n\r\n  \/\/ Geschlecht & Qualit\u00e4t setzen (falls vorhanden)\r\n  if (genderSelect && uset.gender) genderSelect.value = uset.gender;\r\n  if (qualitySelect && uset.quality) qualitySelect.value = uset.quality;\r\n\r\n  \/\/ Stimmen-Dropdown setzen\r\n  if (voiceSelect && uset.voiceId) {\r\n    setTimeout(() => {\r\n      for (let i = 0; i < voiceSelect.options.length; i++) {\r\n        if (voiceSelect.options[i].value === uset.voiceId) {\r\n          voiceSelect.selectedIndex = i;\r\n          break;\r\n        }\r\n      }\r\n    }, 100); \/\/ Ggf. Verz\u00f6gerung, falls der Dropdown erst nach dem Dispatch gebaut wird\r\n  }\r\n}\r\n\r\nasync function loadVoicesAuto(targetLang) {\r\n  const resp = await fetch(ajaxUrl, {\r\n    method: 'POST',\r\n    headers: { 'Content-Type': 'application\/x-www-form-urlencoded' },\r\n    body: new URLSearchParams({ action: 'get_available_voices' })\r\n  });\r\n  const j = await resp.json();\r\n  if (!j.success) throw new Error('Keine Stimmen vom Server');\r\n\r\n  const voices = j.data;\r\n  const dialectKeys = Object.keys(voices).filter(k => k.startsWith(targetLang + '-'));\r\n  if (!dialectKeys.length) return alert('Keine Dialekte f\u00fcr ' + targetLang);\r\n\r\n  const dialectSelect = document.getElementById('dialectSelect');\r\n  const qualitySelect = document.getElementById('qualitySelect');\r\n  const genderSelect  = document.getElementById('genderSelect');\r\n  const voiceSelect   = document.getElementById('voiceSelect');\r\n\r\n  const langNames   = new Intl.DisplayNames([lang], { type: 'language' });\r\n  const regionNames = new Intl.DisplayNames([lang], { type: 'region' });\r\n\r\n  dialectSelect.innerHTML = dialectKeys.map(d => {\r\n    const [lc, cc] = d.split('-');\r\n    return `<option value=\"${d}\">${langNames.of(lc)}${cc ? ' ('+regionNames.of(cc)+')' : ''}<\/option>`;\r\n  }).join('');\r\n  dialectSelect.selectedIndex = 0;\r\n\r\n  \/\/ WICHTIGE NEUE ZEILE!\r\n  availableVoices = Object.values(voices).flatMap(lang => Object.values(lang.dialects).flat());\r\n\r\n  function refreshDropdowns() {\r\n    const dialectVoices = Object.values(voices[dialectSelect.value].dialects).flat();\r\n\r\n    \/\/ Qualit\u00e4t initialisieren\r\n    const qualities = [...new Set(dialectVoices.map(v => v.quality))];\r\n\r\n    const premiumQualities = ['wavenet', 'studio', 'neural2', 'journey', 'news', 'polyglot', 'casual'];\r\n    const allowedLevels = ['premium', 'diamond'];\r\n    const membershipLevel = window.userMembershipLevel || 'basic';\r\n\r\n    qualitySelect.innerHTML = qualities.map(q => {\r\n      return `<option value=\"${q}\">${q}<\/option>`;\r\n    }).join('');\r\n\r\n    \/\/ Standard-Qualit\u00e4t standardm\u00e4\u00dfig ausw\u00e4hlen\r\n    qualitySelect.value = qualities.includes('Standard') ? 'Standard' : qualities[0];\r\n\r\n    \/\/ Geschlecht initialisieren\r\n    const genders = [...new Set(dialectVoices.map(v => v.gender))];\r\n    let genderHtml = `<option value=\"\">${G.any}<\/option>`;\r\n    if (genders.includes('M\u00c4NNLICH')) genderHtml += `<option value=\"M\u00c4NNLICH\">${G.male}<\/option>`;\r\n    if (genders.includes('WEIBLICH')) genderHtml += `<option value=\"WEIBLICH\">${G.female}<\/option>`;\r\n    genderSelect.innerHTML = genderHtml;\r\n\r\n    updateVoiceDropdown(dialectVoices);\r\n  }\r\n\r\n  dialectSelect.addEventListener('change', refreshDropdowns);\r\n  qualitySelect.addEventListener('change', () => {\r\n    const dialectVoices = Object.values(voices[dialectSelect.value].dialects).flat();\r\n    updateVoiceDropdown(dialectVoices);\r\n  });\r\n\r\n  genderSelect.addEventListener('change', () => {\r\n    const dialectVoices = Object.values(voices[dialectSelect.value].dialects).flat();\r\n    updateVoiceDropdown(dialectVoices);\r\n  });\r\n\r\n  refreshDropdowns();\r\n}\r\n\r\nfunction updateVoiceDropdown(dialectVoices) {\r\n    const quality = document.getElementById('qualitySelect').value;\r\n    const gender  = document.getElementById('genderSelect').value;\r\n\r\n    const filteredVoices = dialectVoices.filter(v => \r\n        (!quality || v.quality === quality) &&\r\n        (!gender  || v.gender  === gender) \/\/ Gender jetzt korrekt gefiltert!\r\n    );\r\n\r\n    document.getElementById('voiceSelect').innerHTML = filteredVoices.map(v => {\r\n        const lbl = v.gender === \"M\u00c4NNLICH\" ? G.male : v.gender === \"WEIBLICH\" ? G.female : \"\";\r\n        return `<option value=\"${v.voiceId}\">\r\n                  ${v.voiceId} (${lbl}, ${v.quality})\r\n                <\/option>`;\r\n    }).join('');\r\n\r\n    updateVoiceDropdownAccessibility(availableVoices);\r\n\r\n    if (filteredVoices.length > 0) document.getElementById('voiceSelect').value = filteredVoices[0].voiceId;\r\n}\r\n\r\ndocument.addEventListener('DOMContentLoaded', async () => {\r\n  \/\/ WICHTIG: Erst alle Elemente \u00fcberpr\u00fcfen\r\n  const requiredElements = {\r\n    'target-lang': document.getElementById('target-lang'),\r\n    'translate-btn': document.getElementById('translate-btn'),\r\n    'tts-btn': document.getElementById('tts-btn'),\r\n    'photo-btn': document.getElementById('photo-btn'),\r\n    'upload-btn': document.getElementById('upload-btn'),\r\n    'file-input': document.getElementById('file-input'),\r\n    'fullscreenPlay': document.getElementById('fullscreenPlay'),\r\n    'closeFullscreen': document.getElementById('closeFullscreen'),\r\n    'fontSizeSlider': document.getElementById('fontSizeSlider'),\r\n    'fontLarger': document.getElementById('fontLarger'),\r\n    'fontSmaller': document.getElementById('fontSmaller')\r\n  };\r\n\r\n  \/\/ Debug: Zeige welche Elemente fehlen\r\n  const missingElements = [];\r\n  for (const [name, element] of Object.entries(requiredElements)) {\r\n    if (!element) {\r\n      missingElements.push(name);\r\n      console.error(`\u274c Element nicht gefunden: ${name}`);\r\n    } else {\r\n      console.log(`\u2705 Element gefunden: ${name}`);\r\n    }\r\n  }\r\n\r\n  if (missingElements.length > 0) {\r\n    console.error('\ud83d\udea8 Fehlende Elemente:', missingElements);\r\n    console.error('\ud83d\udd0d \u00dcberpr\u00fcfe dein HTML - diese IDs fehlen!');\r\n    return; \/\/ Stoppe hier wenn wichtige Elemente fehlen\r\n  }\r\n\r\n  \/\/ Jetzt erst die restlichen Variablen setzen\r\n  ajaxUrl = '\/wp-admin\/admin-ajax.php';\r\n  fileInput = requiredElements['file-input'];\r\n  previewImg = document.getElementById('preview-img');\r\n  langBox = document.getElementById('detected-lang');\r\n  translateBtn = requiredElements['translate-btn'];\r\n  ttsBtn = requiredElements['tts-btn'];\r\n  \r\n  \/\/ Lade User-Voice-Settings\r\n  userVoiceSettings = await fetchUserVoices();\r\n\r\n  \/\/ Sprachen laden\r\n  await populateLibreTranslateLanguages();\r\n\r\n  \/\/ Membership-Level laden\r\n  window.userMembershipLevel = await fetchUserMembershipLevel();\r\n\r\n  \/\/ Event Listeners nur hinzuf\u00fcgen wenn Elemente existieren\r\n  console.log('\ud83d\udd27 F\u00fcge Event Listeners hinzu...');\r\n\r\n  \/\/ Photo & Upload Buttons\r\n  if (requiredElements['photo-btn']) {\r\n    requiredElements['photo-btn'].onclick = () => {\r\n      fileInput.setAttribute('capture','environment');\r\n      fileInput.click();\r\n    };\r\n  }\r\n\r\n  if (requiredElements['upload-btn']) {\r\n    requiredElements['upload-btn'].onclick = () => {\r\n      fileInput.removeAttribute('capture');\r\n      fileInput.click();\r\n    };\r\n  }\r\n\r\n  \/\/ File Input Change\r\n  if (fileInput) {\r\n    fileInput.addEventListener('change', async e => {\r\n      const file = e.target.files[0];\r\n      if (!file) return;\r\n\r\n      const reader = new FileReader();\r\n      reader.onload = async ev => {\r\n        const compressedDataUrl = await compressImage(ev.target.result, 600, 0.8);\r\n        previewImg.src = compressedDataUrl;\r\n        previewImg.style.display = 'block';\r\n        const b64 = compressedDataUrl.split(',')[1];\r\n        runOCR(b64);\r\n      };\r\n\r\n      reader.readAsDataURL(file);\r\n    });\r\n  }\r\n\r\n  \/\/ EINZIGER Translate Button Event Listener\r\n  if (translateBtn) {\r\n    translateBtn.addEventListener('click', async () => {\r\n      if (!window.leftParas?.length && !window.rightParas?.length) {\r\n        return alert('Bitte zuerst ein Bild hochladen und OCR ausf\u00fchren!');\r\n      }\r\n\r\n      translateBtn.textContent = getText('translating');\r\n      translateBtn.disabled = true;\r\n\r\n      const target = document.getElementById('target-lang').value;\r\n      const paras = [\r\n        ...window.leftParas.map(para => para.map(b => b.text).join(' ')),\r\n        ...window.rightParas.map(para => para.map(b => b.text).join(' '))\r\n      ];\r\n      const q = paras.join('\\n\\n');\r\n      \r\n      try {\r\n        console.log('\ud83d\udd0d \u00dcbersetze via api.textsnapper.com...');\r\n        \r\n        const resp = await fetch('https:\/\/api.textsnapper.com\/translate', {\r\n          method: 'POST',\r\n          headers: { 'Content-Type': 'application\/json' },\r\n          body: JSON.stringify({ q, source: 'auto', target, format: 'text' })\r\n        });\r\n        \r\n        if (!resp.ok) {\r\n          throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);\r\n        }\r\n        \r\n        const data = await resp.json();\r\n        if (data.error) {\r\n          throw new Error(data.error);\r\n        }\r\n\r\n        const allTranslated = data.translatedText.split('\\n\\n');\r\n        const mid = window.leftParas.length;\r\n        window.translatedLeft = allTranslated.slice(0, mid);\r\n        window.translatedRight = allTranslated.slice(mid);\r\n\r\n        renderTranslatedColumns(window.translatedLeft, window.translatedRight);\r\n\r\n        if (ttsBtn) {\r\n          ttsBtn.style.display = 'inline-block';\r\n        }\r\n        \r\n        const voiceSettings = document.getElementById('voice-settings');\r\n        if (voiceSettings) {\r\n          voiceSettings.style.display = 'block';\r\n        }\r\n\r\n        translatedText = data.translatedText;\r\n\r\n        await loadVoicesAuto(target);\r\n        applyUserVoiceSettingsTrans(target);\r\n\r\n        console.log('\u2705 \u00dcbersetzung via api.textsnapper.com erfolgreich!');\r\n\r\n      } catch (err) {\r\n        console.error('\u274c \u00dcbersetzungsfehler:', err);\r\n        alert(`\u274c \u00dcbersetzungsfehler: ${err.message}\\n\\nBitte versuche es sp\u00e4ter nochmal.`);\r\n      } finally {\r\n        translateBtn.textContent = getText('translate');\r\n        translateBtn.disabled = false;\r\n      }\r\n    });\r\n  }\r\n\r\n  \/\/ TTS Button\r\n  if (ttsBtn) {\r\n    ttsBtn.addEventListener('click', () => {\r\n      if (!translatedText.trim()) return;\r\n      document.getElementById('fullscreenText').textContent = translatedText;\r\n      document.getElementById('fullscreenTTS').style.display = 'flex';\r\n      setTimeout(()=>document.getElementById('fullscreenPlay').click(), 200);\r\n    });\r\n  }\r\n\r\n  \/\/ Fullscreen Controls\r\n  if (requiredElements['closeFullscreen']) {\r\n    requiredElements['closeFullscreen'].onclick = () => {\r\n      document.getElementById('fullscreenTTS').style.display = 'none';\r\n      if (window.ttsAudio) window.ttsAudio.pause();\r\n      clearInterval(ttsInterval);\r\n    };\r\n  }\r\n\r\n  \/\/ Font Size Controls\r\n  if (requiredElements['fontSizeSlider']) {\r\n    requiredElements['fontSizeSlider'].oninput = () => {\r\n      document.getElementById('fullscreenText').style.fontSize = requiredElements['fontSizeSlider'].value+'px';\r\n    };\r\n  }\r\n\r\n  if (requiredElements['fontLarger']) {\r\n    requiredElements['fontLarger'].onclick = () => {\r\n      const slider = requiredElements['fontSizeSlider'];\r\n      if (slider) {\r\n        slider.value = Math.min(80, +slider.value+4);\r\n        document.getElementById('fullscreenText').style.fontSize = slider.value+'px';\r\n      }\r\n    };\r\n  }\r\n\r\n  if (requiredElements['fontSmaller']) {\r\n    requiredElements['fontSmaller'].onclick = () => {\r\n      const slider = requiredElements['fontSizeSlider'];\r\n      if (slider) {\r\n        slider.value = Math.max(16, +slider.value-4);\r\n        document.getElementById('fullscreenText').style.fontSize = slider.value+'px';\r\n      }\r\n    };\r\n  }\r\n\r\n  \/\/ Fullscreen Play Button mit TTS-Funktionalit\u00e4t\r\n  if (requiredElements['fullscreenPlay']) {\r\n    requiredElements['fullscreenPlay'].onclick = async function() {\r\n      const btn = this;\r\n      const fullscreenText = document.getElementById('fullscreenText');\r\n      const textLength = fullscreenText.textContent.length;\r\n\r\n      \/\/ Credit-Check\r\n      const [monthlyRes, packageRes] = await Promise.all([\r\n        fetch(window.ajaxUrl || '\/wp-admin\/admin-ajax.php', {\r\n          method: 'POST',\r\n          credentials: 'include',\r\n          headers: { 'Content-Type': 'application\/x-www-form-urlencoded' },\r\n          body: new URLSearchParams({ action: 'get_user_credit_count' })\r\n        }).then(res => res.json()),\r\n        fetch(window.ajaxUrl || '\/wp-admin\/admin-ajax.php', {\r\n          method: 'POST',\r\n          credentials: 'include',\r\n          headers: { 'Content-Type': 'application\/x-www-form-urlencoded' },\r\n          body: new URLSearchParams({ action: 'get_user_package_credits' })\r\n        }).then(res => res.json())\r\n      ]);\r\n\r\n      const monthlyCredits = monthlyRes.success ? monthlyRes.data.monthly_credits_remaining : 0;\r\n      const packageCredits = packageRes.success ? packageRes.data.credits : 0;\r\n      const totalCreditsAvailable = monthlyCredits + packageCredits;\r\n\r\n      if (totalCreditsAvailable < textLength) {\r\n        const upgradeMsg = lang === 'en' \r\n          ? `\u26a0\ufe0f Insufficient credits. Monthly credits: ${monthlyCredits}, Package credits: ${packageCredits}.`\r\n          : `\u26a0\ufe0f Unzureichende Credits. Monatliche Credits: ${monthlyCredits}, Paket-Credits: ${packageCredits}.`;\r\n\r\n        const upgradeText = lang === 'en' ? 'Upgrade now' : 'Jetzt upgraden';\r\n        const upgradeUrl = 'https:\/\/textsnapper.com\/pricing\/';\r\n\r\n        const popup = document.createElement('div');\r\n        popup.style.position = 'fixed';\r\n        popup.style.top = '0';\r\n        popup.style.left = '0';\r\n        popup.style.width = '100%';\r\n        popup.style.height = '100%';\r\n        popup.style.backgroundColor = 'rgba(0,0,0,0.7)';\r\n        popup.style.display = 'flex';\r\n        popup.style.flexDirection = 'column';\r\n        popup.style.justifyContent = 'center';\r\n        popup.style.alignItems = 'center';\r\n        popup.style.zIndex = '10000';\r\n\r\n        popup.innerHTML = `\r\n          <div style=\"background:#fff;padding:20px;border-radius:8px;max-width:400px;text-align:center;\">\r\n            <p style=\"margin-bottom:20px;\">${upgradeMsg}<\/p>\r\n            <a href=\"${upgradeUrl}\" style=\"display:inline-block;padding:10px 20px;background:#2F5591;color:#fff;border-radius:5px;text-decoration:none;font-weight:bold;\">${upgradeText}<\/a>\r\n            <button style=\"margin-top:15px;background:none;border:none;color:#888;cursor:pointer;\" onclick=\"this.parentElement.parentElement.remove()\">Schlie\u00dfen<\/button>\r\n          <\/div>\r\n        `;\r\n\r\n        document.body.appendChild(popup);\r\n        btn.disabled = false;\r\n        btn.textContent = getText('read-aloud');\r\n        return;\r\n      }\r\n\r\n      const voiceId = document.getElementById('voiceSelect').value;\r\n      const rate = parseFloat(document.getElementById('speedSelect').value) || 1;\r\n      const dialect = document.getElementById('dialectSelect').value;\r\n      const gender = document.getElementById('genderSelect').value;\r\n      const quality = document.getElementById('qualitySelect').value;\r\n\r\n      if (!voiceId) return alert('Bitte Stimme w\u00e4hlen!');\r\n\r\n      \/\/ Pause & Play Handling\r\n      if (window.ttsAudio) {\r\n        if (!window.ttsAudio.paused) {\r\n          window.ttsAudio.pause(); \r\n          btn.textContent = '\u25b6\ufe0f Fortsetzen';\r\n          clearInterval(ttsInterval); \r\n        } else {\r\n          window.ttsAudio.play(); \r\n          btn.textContent = '\u23f8\ufe0f Pause';\r\n          startHighlighting();\r\n        }\r\n        return;\r\n      }\r\n\r\n      btn.disabled = true;\r\n      btn.textContent = '\u23f3 Lade\u2026';\r\n\r\n      try {\r\n        const usePlainText = \/^.+-(Chirp|Chirp3)(-.+)?$\/i.test(voiceId);\r\n        const contentKey = usePlainText ? 'text' : 'ssml';\r\n        const contentValue = usePlainText ? translatedText : `<speak>${translatedText}<\/speak>`;\r\n\r\n        const resp = await fetch(ajaxUrl, {\r\n          method: 'POST',\r\n          headers: { 'Content-Type':'application\/x-www-form-urlencoded' },\r\n          body: new URLSearchParams({\r\n            action: 'process_tts',\r\n            [contentKey]: contentValue,\r\n            voiceId, \r\n            speakingRate: rate,\r\n            languageCode: dialect,\r\n            gender: gender,\r\n            quality: quality,\r\n            voice_quality: quality\r\n          })\r\n        });\r\n\r\n        const j = await resp.json();\r\n        if (!j.success) throw new Error(j.data);\r\n\r\n        window.ttsAudio = new Audio('data:audio\/mp3;base64,' + j.data.audioContent);\r\n\r\n        window.ttsAudio.addEventListener('loadedmetadata', () => {\r\n          startHighlighting();\r\n          window.ttsAudio.play();\r\n          document.dispatchEvent(new CustomEvent('ttsUsageUpdated'));\r\n          btn.disabled = false;\r\n          btn.textContent = '\u23f8\ufe0f Pause';\r\n        });\r\n\r\n        window.ttsAudio.addEventListener('ended', () => {\r\n          btn.textContent = '\ud83d\udd0a Vorlesen';\r\n          clearInterval(ttsInterval);\r\n          currentWord = 0;\r\n          window.ttsAudio = null;\r\n        });\r\n\r\n      } catch (e) {\r\n        console.error('TTS-Fehler', e);\r\n        alert('Sprachausgabe fehlgeschlagen');\r\n        btn.disabled = false;\r\n        btn.textContent = '\ud83d\udd0a Vorlesen';\r\n      }\r\n\r\n      function startHighlighting() {\r\n        clearInterval(ttsInterval);\r\n        const words = translatedText.trim().split(\/\\s+\/);\r\n        const dur = window.ttsAudio.duration;\r\n        const interval = dur \/ words.length * 1000;\r\n\r\n        const fullscreenText = document.getElementById('fullscreenText');\r\n        fullscreenText.innerHTML = words.map((word, idx) => \r\n          `<span id=\"w${idx}\">${word} <\/span>`\r\n        ).join('');\r\n\r\n        currentWord = Math.floor(window.ttsAudio.currentTime \/ (dur \/ words.length));\r\n        const HIGHLIGHT_BEFORE = 4;\r\n        const HIGHLIGHT_AFTER = 8;\r\n\r\n        ttsInterval = setInterval(() => {\r\n          words.forEach((_, idx) => {\r\n            document.getElementById(`w${idx}`).classList.remove('highlight');\r\n          });\r\n\r\n          const startIdx = Math.max(0, currentWord - HIGHLIGHT_BEFORE);\r\n          const endIdx = Math.min(words.length - 1, currentWord + HIGHLIGHT_AFTER);\r\n\r\n          for (let i = startIdx; i <= endIdx; i++) {\r\n            document.getElementById(`w${i}`).classList.add('highlight');\r\n          }\r\n\r\n          document.getElementById(`w${currentWord}`).scrollIntoView({ behavior: 'smooth', block: 'center' });\r\n\r\n          currentWord++;\r\n          if (currentWord >= words.length) clearInterval(ttsInterval);\r\n\r\n        }, interval);\r\n      }\r\n    };\r\n  }\r\n\r\n  console.log('\u2705 Alle Event Listeners erfolgreich hinzugef\u00fcgt!');\r\n\r\n}); \/\/ Ende DOMContentLoaded\r\n\r\nasync function populateLibreTranslateLanguages() {\r\n  const dropdown = document.getElementById('target-lang');\r\n  if (!dropdown) return;\r\n\r\n  try {\r\n    \/\/ ERSTE PRIORIT\u00c4T: HTTPS api.textsnapper.com (sollte jetzt funktionieren)\r\n    console.log('\ud83d\udd0d Lade Sprachen von api.textsnapper.com (HTTPS)...');\r\n    \r\n    const res = await fetch('https:\/\/api.textsnapper.com\/languages', {\r\n      method: 'GET',\r\n      headers: {\r\n        'Accept': 'application\/json',\r\n        'Content-Type': 'application\/json'\r\n      }\r\n    });\r\n\r\n    if (res.ok) {\r\n      const data = await res.json();\r\n      \r\n      if (Array.isArray(data)) {\r\n        \/\/ ERFOLG - verwende api.textsnapper.com Daten\r\n        console.log('\u2705 api.textsnapper.com funktioniert perfekt!');\r\n        \r\n        const langNames = new Intl.DisplayNames([document.documentElement.lang.startsWith('en') ? 'en' : 'de'], { type: 'language' });\r\n        dropdown.innerHTML = '';\r\n\r\n        data.forEach(lang => {\r\n          const name = langNames.of(lang.code) || lang.name || lang.code;\r\n          const option = document.createElement('option');\r\n          option.value = lang.code;\r\n          option.textContent = name.charAt(0).toUpperCase() + name.slice(1);\r\n          dropdown.appendChild(option);\r\n        });\r\n\r\n        dropdown.value = 'en';\r\n        console.log(`\u2705 ${data.length} Sprachen von api.textsnapper.com geladen`);\r\n        return; \/\/ Beende erfolgreich\r\n      }\r\n    }\r\n    \r\n    throw new Error(`HTTPS API-Fehler: ${res.status} ${res.statusText}`);\r\n\r\n  } catch (err) {\r\n    console.warn('\u26a0\ufe0f api.textsnapper.com nicht erreichbar:', err.message);\r\n    console.log('\ud83d\udd04 Versuche WordPress-Proxy...');\r\n    \r\n    \/\/ FALLBACK 1: WordPress-Proxy\r\n    try {\r\n      const proxyRes = await fetch(ajaxUrl, {\r\n        method: 'POST',\r\n        headers: { 'Content-Type': 'application\/x-www-form-urlencoded' },\r\n        body: new URLSearchParams({ action: 'get_libretranslate_languages' })\r\n      });\r\n\r\n      if (proxyRes.ok) {\r\n        const result = await proxyRes.json();\r\n        if (result.success && Array.isArray(result.data)) {\r\n          console.log('\u2705 WordPress-Proxy funktioniert!');\r\n          \r\n          const langNames = new Intl.DisplayNames([document.documentElement.lang.startsWith('en') ? 'en' : 'de'], { type: 'language' });\r\n          dropdown.innerHTML = '';\r\n\r\n          result.data.forEach(lang => {\r\n            const name = langNames.of(lang.code) || lang.name || lang.code;\r\n            const option = document.createElement('option');\r\n            option.value = lang.code;\r\n            option.textContent = name.charAt(0).toUpperCase() + name.slice(1);\r\n            dropdown.appendChild(option);\r\n          });\r\n\r\n          dropdown.value = 'en';\r\n          console.log(`\u2705 ${result.data.length} Sprachen via WordPress-Proxy geladen`);\r\n          return; \/\/ Erfolgreich beendet\r\n        }\r\n      }\r\n    } catch (proxyErr) {\r\n      console.warn('\u26a0\ufe0f WordPress-Proxy fehlgeschlagen:', proxyErr.message);\r\n    }\r\n\r\n    \/\/ FALLBACK 2: Statische Liste (letzter Ausweg)\r\n    console.log('\ud83d\udd04 Verwende statische Fallback-Sprachliste...');\r\n    \r\n    const fallbackLanguages = [\r\n      { code: 'en', name: 'English' },\r\n      { code: 'de', name: 'Deutsch' },\r\n      { code: 'es', name: 'Espa\u00f1ol' },\r\n      { code: 'fr', name: 'Fran\u00e7ais' },\r\n      { code: 'it', name: 'Italiano' },\r\n      { code: 'pt', name: 'Portugu\u00eas' },\r\n      { code: 'ru', name: '\u0420\u0443\u0441\u0441\u043a\u0438\u0439' },\r\n      { code: 'ja', name: '\u65e5\u672c\u8a9e' },\r\n      { code: 'ko', name: '\ud55c\uad6d\uc5b4' },\r\n      { code: 'zh', name: '\u4e2d\u6587' },\r\n      { code: 'ar', name: '\u0627\u0644\u0639\u0631\u0628\u064a\u0629' },\r\n      { code: 'hi', name: '\u0939\u093f\u0928\u094d\u0926\u0940' },\r\n      { code: 'tr', name: 'T\u00fcrk\u00e7e' },\r\n      { code: 'pl', name: 'Polski' },\r\n      { code: 'nl', name: 'Nederlands' }\r\n    ];\r\n\r\n    const langNames = new Intl.DisplayNames([document.documentElement.lang.startsWith('en') ? 'en' : 'de'], { type: 'language' });\r\n    dropdown.innerHTML = '';\r\n\r\n    fallbackLanguages.forEach(lang => {\r\n      const name = langNames.of(lang.code) || lang.name || lang.code;\r\n      const option = document.createElement('option');\r\n      option.value = lang.code;\r\n      option.textContent = name.charAt(0).toUpperCase() + name.slice(1);\r\n      dropdown.appendChild(option);\r\n    });\r\n\r\n    dropdown.value = 'en';\r\n    console.log('\u26a0\ufe0f Fallback-Sprachen geladen (alle Hauptquellen nicht erreichbar)');\r\n  }\r\n}\r\n\r\nasync function performTranslation(text, sourceLang, targetLang) {\r\n  const strategies = [\r\n    {\r\n      name: 'HTTPS api.textsnapper.com',\r\n      url: 'https:\/\/api.textsnapper.com\/translate'\r\n    },\r\n    {\r\n      name: 'WordPress Proxy (falls implementiert)',\r\n      url: ajaxUrl,\r\n      method: 'POST',\r\n      body: new URLSearchParams({ \r\n        action: 'ts_translate_text',\r\n        q: text,\r\n        source: sourceLang,\r\n        target: targetLang\r\n      })\r\n    }\r\n  ];\r\n\r\n  for (const strategy of strategies) {\r\n    try {\r\n      console.log(`\ud83d\udd0d \u00dcbersetzung via: ${strategy.name}`);\r\n      \r\n      let response;\r\n      if (strategy.method === 'POST') {\r\n        response = await fetch(strategy.url, {\r\n          method: 'POST',\r\n          headers: { 'Content-Type': 'application\/x-www-form-urlencoded' },\r\n          body: strategy.body\r\n        });\r\n      } else {\r\n        response = await fetch(strategy.url, {\r\n          method: 'POST',\r\n          headers: { 'Content-Type': 'application\/json' },\r\n          body: JSON.stringify({ q: text, source: sourceLang, target: targetLang, format: 'text' })\r\n        });\r\n      }\r\n\r\n      if (!response.ok) {\r\n        throw new Error(`HTTP ${response.status}`);\r\n      }\r\n\r\n      const data = await response.json();\r\n      \r\n      if (strategy.name.includes('WordPress') && data.success) {\r\n        return { translatedText: data.translatedText };\r\n      } else if (data.translatedText) {\r\n        return data;\r\n      } else {\r\n        throw new Error('Keine \u00dcbersetzung erhalten');\r\n      }\r\n\r\n    } catch (err) {\r\n      console.error(`\u274c ${strategy.name} fehlgeschlagen:`, err.message);\r\n      continue;\r\n    }\r\n  }\r\n  \r\n  throw new Error('Alle \u00dcbersetzungsstrategien fehlgeschlagen');\r\n}\r\n\r\nasync function fetchUserMembershipLevel() {\r\n  let fd = new URLSearchParams();\r\n  fd.set(\"action\", \"get_user_membership\");\r\n  try {\r\n    let response = await fetch(window.ajaxurl || '\/wp-admin\/admin-ajax.php', {\r\n      method: \"POST\",\r\n      headers: { \"Content-Type\": \"application\/x-www-form-urlencoded\" },\r\n      body: fd,\r\n      credentials: \"include\"\r\n    });\r\n    let result = await response.json();\r\n\r\n    if (result.success && result.data.membership_name) {\r\n      const membershipName = result.data.membership_name.toLowerCase();\r\n\r\n      if (membershipName.includes('premium')) {\r\n        return 'premium';\r\n      } else if (membershipName.includes('diamond')) {\r\n        return 'diamond';\r\n      } else {\r\n        return 'basic';\r\n      }\r\n\r\n    } else {\r\n      return 'basic';\r\n    }\r\n  } catch (e) {\r\n    console.error(\"Fehler beim Abfragen des Membership-Levels:\", e);\r\n    return 'basic';\r\n  }\r\n}\r\n\r\nasync function updateVoiceDropdownAccessibility(allVoices) {\r\n  const voiceSelect = document.getElementById('voiceSelect');\r\n\r\n  voiceSelect.querySelectorAll(\"option\").forEach(option => {\r\n    option.disabled = false;\r\n    option.textContent = option.textContent.replace(\" (Premium)\", \"\");\r\n  });\r\n}\r\n\r\n\/\/ Hilfsfunktion, um Qualit\u00e4t aus voiceId zu ermitteln\r\nfunction getVoiceQuality(voiceId, allVoices) {\r\n  const voice = allVoices.find(v => v.voiceId === voiceId);\r\n  return voice ? voice.quality.toLowerCase() : \"standard\";\r\n}\r\n\r\n\/\/ Hilfsfunktion: komprimiere DataURL auf maxWidth px\r\nfunction compressImage(dataUrl, maxWidth = 600, quality = 0.8) {\r\n  return new Promise(resolve => {\r\n    const img = new Image();\r\n    img.onload = () => {\r\n      const scale = Math.min(1, maxWidth \/ img.width);\r\n      const w = img.width * scale;\r\n      const h = img.height * scale;\r\n      const canvas = document.createElement('canvas');\r\n      canvas.width = w;\r\n      canvas.height = h;\r\n      const ctx = canvas.getContext('2d');\r\n      ctx.drawImage(img, 0, 0, w, h);\r\n      resolve(canvas.toDataURL('image\/jpeg', quality));\r\n    };\r\n    img.src = dataUrl;\r\n  });\r\n}\r\n\r\nfunction annotateBlocks(blocks) {\r\n  return blocks\r\n    .filter(b => b && b.text && b.vertices && b.vertices.length === 4)\r\n    .map(b => {\r\n      const xs = b.vertices.map(v => v.x || 0);\r\n      const ys = b.vertices.map(v => v.y || 0);\r\n      const xMean = xs.reduce((a, c) => a + c, 0) \/ xs.length;\r\n      const yMean = ys.reduce((a, c) => a + c, 0) \/ ys.length;\r\n      const height = Math.max(...ys) - Math.min(...ys);\r\n      return { text: b.text.trim(), xMean, yMean, height };\r\n    });\r\n}\r\n\r\n\/\/ 2) Spalten ermitteln \u00fcber einfachen X-Abstand\r\nfunction detectColumns(ann, imgWidth) {\r\n  \/\/ Schwelle auf 1\/3 der Breite w\u00e4hlen\r\n  const threshold = imgWidth \/ 3;\r\n  const cols = [[], []];\r\n  ann.forEach(b => {\r\n    if (b.xMean < threshold) cols[0].push(b);\r\n    else cols[1].push(b);\r\n  });\r\n  \/\/ jede Spalte nach Y sortieren\r\n  cols.forEach(col => col.sort((a, b) => a.yMean - b.yMean));\r\n  return cols;\r\n}\r\n\r\nfunction groupParagraphs(sortedBlocks) {\r\n  if (!sortedBlocks.length) return []; \/\/ leere Spalten verhindern\r\n  const avgH = sortedBlocks.reduce((sum, b) => sum + b.height, 0) \/ sortedBlocks.length;\r\n  const vGap = avgH * 1.5;\r\n  const paras = [];\r\n  let current = [sortedBlocks[0]];\r\n  for (let i = 1; i < sortedBlocks.length; i++) {\r\n    const gap = sortedBlocks[i].yMean - sortedBlocks[i-1].yMean;\r\n    if (gap > vGap) {\r\n      paras.push(current);\r\n      current = [sortedBlocks[i]];\r\n    } else {\r\n      current.push(sortedBlocks[i]);\r\n    }\r\n  }\r\n  paras.push(current);\r\n  return paras;\r\n}\r\n\r\nasync function runOCR(base64) {\r\n  \r\n  langBox.textContent = '';\r\n  ttsBtn.style.display = 'none';\r\n  document.getElementById('voice-settings').style.display = 'none';\r\n  const structOut = document.getElementById('structured-output');\r\n  structOut.innerHTML = '';\r\n\r\n  try {\r\n    \/\/ 1) OCR-Request\r\n    const ocrResp = await fetch(ajaxUrl, {\r\n      method: 'POST',\r\n      headers: { 'Content-Type': 'application\/x-www-form-urlencoded' },\r\n      body: new URLSearchParams({\r\n        action: 'process_ocr',\r\n        image_data: base64\r\n      })\r\n    });\r\n\r\n    if (!ocrResp.ok) throw new Error(`OCR HTTP-Fehler: ${ocrResp.status}`);\r\n    const ocrJson = await ocrResp.json();\r\n    if (!ocrJson.success || !Array.isArray(ocrJson.data.boxes)) {\r\n      throw new Error('Keine OCR-Daten erhalten');\r\n    }\r\n\r\n    \/\/ 2) Zentroid\/H\u00f6he berechnen\r\n    const annotated = annotateBlocks(ocrJson.data.boxes);\r\n\r\n    \/\/ 3) Spalten erkennen (1\/3 Bildbreite)\r\n    const [leftCol, rightCol] = detectColumns(annotated, previewImg.width);\r\n\r\n    \/\/ 4) Abs\u00e4tze clustern\r\n    const leftParas  = groupParagraphs(leftCol);\r\n    const rightParas = groupParagraphs(rightCol);\r\n    window.leftParas  = leftParas;\r\n    window.rightParas = rightParas;\r\n    console.log(\"Left Paras:\", leftParas);\r\n    console.log(\"Right Paras:\", rightParas);\r\n\r\n    \/\/ 5) Strukturiert anzeigen\r\n    renderStructuredColumns(leftParas, rightParas, 'structured-output');\r\n\r\n    \/\/ 6) Sprache anzeigen (OCR-Response + FastText-Fallback)\r\n    let fullText = [...leftParas, ...rightParas].flat().map(b => b.text).join(' ');\r\n    if (ocrJson.data.locale) {\r\n      const code2 = ocrJson.data.locale.substr(0,2).toLowerCase();\r\n      const name = languageNameDisplay.of(code2) || ocrJson.data.locale;\r\n      langBox.textContent = `${getText('detected-prefix')} ${name}`;\r\n      langBox.style.display = 'block';\r\n    } else {\r\n      \/\/ FastText-Fallback aufrufen\r\n      try {\r\n        const langResp = await fetch('https:\/\/api.textsnapper.com\/detect_language', {\r\n          method: 'POST',\r\n          headers: {'Content-Type': 'application\/json'},\r\n          body: JSON.stringify({text: fullText})\r\n        });\r\n        const langData = await langResp.json();\r\n        if (langData.success && langData.language) {\r\n          const code2 = langData.language.toLowerCase();\r\n          const name = languageNameDisplay.of(code2) || code2;\r\n          langBox.textContent = `${getText('detected-prefix')} ${name}`;\r\n          langBox.style.display = 'block';\r\n        } else {\r\n          langBox.textContent = `${getText('detected-prefix')} unbekannt`;\r\n          langBox.style.display = 'block';\r\n        }\r\n      } catch (langError) {\r\n        console.error('Spracherkennungsfehler:', langError);\r\n        langBox.textContent = `${getText('detected-prefix')} unbekannt`;\r\n        langBox.style.display = 'block';\r\n      }\r\n    }\r\n\r\n    \/\/ 7) \u00dcbersetzen-Button freischalten\r\n    translateBtn.disabled = false;\r\n    document.dispatchEvent(new Event('ocrUsageUpdated'));\r\n\r\n  } catch (err) {\r\n    console.error('runOCR Error:', err);\r\n    langBox.textContent = '';\r\n  }\r\n}\r\n\r\nfunction renderStructuredColumns(leftParas, rightParas, containerId) {\r\n  const colsWrap = document.getElementById(containerId);\r\n  colsWrap.innerHTML = '';\r\n\r\n  [leftParas, rightParas].forEach(paras => {\r\n    const colDiv = document.createElement('div');\r\n    colDiv.style.flex = '1';\r\n\r\n    paras.forEach(para => {\r\n      if (!para.length) return; \/\/ \ud83d\udc48 Hier hinzugef\u00fcgt, \u00fcberspringt leere Paragraphen!\r\n\r\n      const isTitle = para[0].height > (\r\n        para.slice(1).reduce((s,b)=>s+b.height, 0) \/ Math.max(1, para.length-1)\r\n      ) * 1.2;\r\n\r\n      if (isTitle) {\r\n        const h3 = document.createElement('h3');\r\n        h3.textContent = para.map(b=>b.text).join(' ');\r\n        colDiv.appendChild(h3);\r\n      } else {\r\n        const p = document.createElement('p');\r\n        p.textContent = para.map(b=>b.text).join(' ');\r\n        colDiv.appendChild(p);\r\n      }\r\n    });\r\n\r\n    colsWrap.appendChild(colDiv);\r\n  });\r\n}\r\n\r\n\/**\r\n * Linke und rechte Liste von Strings rendern\r\n *\/\r\nfunction renderTranslatedColumns(leftArr, rightArr) {\r\n  const colsWrap = document.getElementById('structured-output');\r\n  colsWrap.innerHTML = '';\r\n\r\n  [leftArr, rightArr].forEach(arr => {\r\n    const colDiv = document.createElement('div');\r\n    colDiv.style.flex = '1';\r\n    arr.forEach(text => {\r\n      const p = document.createElement('p');\r\n      p.textContent = text;\r\n      colDiv.appendChild(p);\r\n    });\r\n    colsWrap.appendChild(colDiv);\r\n  });\r\n}\r\n\r\nasync function updateVoiceOptions(allVoices, autoSelect = false) {\r\n  const quality = document.getElementById('qualitySelect').value;\r\n  const gender  = document.getElementById('genderSelect').value;\r\n  const voiceSelect = document.getElementById('voiceSelect');\r\n\r\n  \/\/ Stimmen nach Qualit\u00e4t+Geschlecht filtern\r\n  const voices = allVoices.filter(v => \r\n    (!quality || v.quality === quality) &&\r\n    (!gender  || v.gender  === gender)\r\n  );\r\n\r\n  \/\/ Dropdown bef\u00fcllen mit lokalisierten Gender-Labels\r\n  voiceSelect.innerHTML = voices\r\n    .map(v => {\r\n      const lbl = v.gender === \"MALE\"   ? G.male\r\n                : v.gender === \"FEMALE\" ? G.female\r\n                : \"\";\r\n      return `<option value=\"${v.voiceId}\">\r\n                ${v.voiceId} (${lbl}, ${v.quality})\r\n              <\/option>`;\r\n    })\r\n    .join('');\r\n\r\n  \/\/ Premium-Stimmen ggf. deaktivieren\r\n  await updateVoiceDropdownAccessibility(allVoices);\r\n\r\n  \/\/ Erlaube autoSelect = true \u2192 erste erlaubte Stimme vorausw\u00e4hlen\r\n  if (autoSelect && voices.length > 0) {\r\n    for (let opt of voiceSelect.options) {\r\n      if (!opt.disabled) { voiceSelect.value = opt.value; break; }\r\n    }\r\n  }\r\n}\r\n\r\nasync function checkUserOCRLimit() {\r\n  const response = await fetch('\/wp-admin\/admin-ajax.php', {\r\n    method: \"POST\",\r\n    headers: { \"Content-Type\": \"application\/x-www-form-urlencoded\" },\r\n    credentials: \"include\",\r\n    body: new URLSearchParams({ action: \"get_user_ocr_count\" })\r\n  });\r\n  const result = await response.json();\r\n  \r\n  if (result.success) {\r\n    const { used_ocr, ocr_limit } = result.data;\r\n    \r\n    if (ocr_limit !== -1 && used_ocr >= ocr_limit) {\r\n      document.getElementById('photo-btn').disabled = true;\r\n      document.getElementById('upload-btn').disabled = true;\r\n\r\n      document.getElementById('photo-btn').style.background = '#ccc';\r\n      document.getElementById('upload-btn').style.background = '#ccc';\r\n\r\n      const upgradeNotice = document.createElement('div');\r\n      upgradeNotice.style.color = '#b00000';\r\n      upgradeNotice.style.fontWeight = 'bold';\r\n      upgradeNotice.style.marginTop = '15px';\r\n      upgradeNotice.textContent = '\u26a0\ufe0f Keine Uploads mehr m\u00f6glich. Bitte Upgrade durchf\u00fchren.';\r\n      \r\n      document.getElementById('ocr-widget').prepend(upgradeNotice);\r\n    }\r\n  } else {\r\n    console.error('Fehler bei OCR-Limitabfrage:', result.data);\r\n  }\r\n}\r\n\r\n\/\/ Pr\u00fcfe OCR-Limit nach dem DOM-Laden\r\ncheckUserOCRLimit();\r\n\r\n<\/script>\r\n\r\n<\/body>\r\n<\/html>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-a911135 elementor-widget elementor-widget-text-editor\" data-id=\"a911135\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p>Click \"Take Photo\" or \"Upload Image\" to select a picture containing text. Choose your target language and confirm by clicking \"Translate\". You can have the translated text read aloud to you, individually adjusting voice, dialect, speed, and quality.<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-6d476ce elementor-widget elementor-widget-html\" data-id=\"6d476ce\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<div id=\"credit-count-notice-widget\" style=\"text-align:center; margin-top:20px;\">\n  <span id=\"credits-left-notice\">Lade Credits...<\/span>\n  <br>\n  <span id=\"ocr-left-notice\" style=\"display:none;\">Lade OCR-Limits...<\/span>\n  <div id=\"upgrade-button-container\" style=\"display:none;\">\n    <a href=\"https:\/\/textsnapper.com\/en\/pricing\/\" class=\"upgrade-btn\">upgrade now<\/a>\n  <\/div>\n<\/div>\n\n<style>\n  #credits-left-notice, #ocr-left-notice {\n    font-family: 'Atkinson Hyperlegible', Arial, sans-serif;\n    font-size: 16px;\n    color: #555;\n  }\n\n  .warning {\n    color: #f44336;\n    font-weight: bold;\n  }\n\n  .unlimited {\n    color: #4CAF50;\n    font-weight: bold;\n  }\n\n  .upgrade-btn {\n    display: inline-block;\n    padding: 10px 20px;\n    background: #2F5591;\n    color: #FFFFFF;\n    font-weight: bold;\n    border: none;\n    border-radius: 4px;\n    text-decoration: none;\n    cursor: pointer;\n    transition: background 0.3s, transform 0.2s ease;\n    margin-top: 10px;\n  }\n\n  .upgrade-btn:hover {\n    background: #24467a;\n    transform: translateY(-2px);\n  }\n<\/style>\n\n<script>\ndocument.addEventListener('DOMContentLoaded', async function () {\n  const lang = document.documentElement.lang.startsWith('en') ? 'en' : 'de';\n  const noticeEl = document.getElementById('credits-left-notice');\n  const ocrEl = document.getElementById('ocr-left-notice');\n  const upgradeBtnContainer = document.getElementById('upgrade-button-container');\n\n  const translations = {\n    de: {\n      loading: 'Lade Credits...',\n      base_monthly: 'Monatliche Credits verf\u00fcgbar',\n      base_package: 'Paket-Credits verf\u00fcgbar (g\u00fcltig bis',\n      unlimited: 'Verf\u00fcgbare uploads: unbegrenzt \u2714\ufe0f',\n      warning: '\u26a0\ufe0f Limit fast erreicht!',\n      error: 'Fehler beim Laden.',\n      no_credits: 'Keine Credits verf\u00fcgbar.',\n      upgrade_now: 'Jetzt upgraden',\n      ocr_loading: 'Lade OCR-Limits...',\n      ocr_available: 'OCR-Uploads verf\u00fcgbar diesen Monat',\n      ocr_no_available: 'Keine OCR-Uploads mehr verf\u00fcgbar.'\n    },\n    en: {\n      loading: 'Loading credits...',\n      base_monthly: 'Monthly credits available',\n      base_package: 'Package credits available (valid until',\n      unlimited: 'Available uploads: unlimited \u2714\ufe0f',\n      warning: '\u26a0\ufe0f Limit almost reached!',\n      error: 'Error loading.',\n      no_credits: 'No credits available.',\n      upgrade_now: 'Upgrade now',\n      ocr_loading: 'Loading OCR limits...',\n      ocr_available: 'OCR uploads available this month',\n      ocr_no_available: 'No OCR uploads left.'\n    }\n  };\n\n  function t(key) {\n    return translations[lang][key];\n  }\n\n  async function fetchAjax(action) {\n    const res = await fetch(window.ajaxurl || '\/wp-admin\/admin-ajax.php', {\n      method: 'POST',\n      credentials: 'include',\n      headers: { \"Content-Type\": \"application\/x-www-form-urlencoded\" },\n      body: new URLSearchParams({ action })\n    });\n    return res.json();\n  }\n\n  let showUpgradeButton = false;\n\n  async function updateCreditNotice() {\n    noticeEl.classList.remove('warning', 'unlimited');\n    noticeEl.textContent = t('loading');\n\n    try {\n      const [monthlyRes, packageRes] = await Promise.all([\n        fetchAjax('get_user_credit_count'),\n        fetchAjax('get_user_package_credits')\n      ]);\n\n      if (!monthlyRes.success || !packageRes.success) {\n        noticeEl.textContent = t('error');\n        return;\n      }\n\n      const monthlyLeft = monthlyRes.data.monthly_credits_remaining;\n      const packageLeft = packageRes.data.credits;\n      const packageExpires = packageRes.data.expires;\n\n      let totalCredits = monthlyLeft + packageLeft;\n      let messages = [];\n\n      if (monthlyLeft === -1) {\n        noticeEl.textContent = t('unlimited');\n        noticeEl.classList.add('unlimited');\n        return;\n      }\n\n      messages.push(`${t('base_monthly')}: ${monthlyLeft}`);\n\n      if (packageLeft > 0) {\n        messages.push(`${t('base_package')} ${packageExpires}): ${packageLeft}`);\n      }\n\n      noticeEl.textContent = messages.join(' | ');\n\n      if (totalCredits <= 500) {\n        noticeEl.classList.add('warning');\n        noticeEl.innerHTML += `<br>${t('warning')}`;\n        showUpgradeButton = true;\n      }\n\n      if (totalCredits <= 0) {\n        noticeEl.classList.add('warning');\n        noticeEl.textContent = `${t('no_credits')}`;\n        showUpgradeButton = true;\n      }\n\n      toggleUpgradeButton();\n    } catch (e) {\n      noticeEl.textContent = t('error');\n    }\n  }\n\n  async function updateOCRNotice() {\n    ocrEl.style.display = 'inline';\n    ocrEl.textContent = t('ocr_loading');\n    ocrEl.classList.remove('warning');\n\n    try {\n      const res = await fetchAjax('get_user_ocr_count');\n      if (!res.success) {\n        ocrEl.textContent = t('error');\n        return;\n      }\n\n      const ocrLeft = res.data.ocr_limit - res.data.used_ocr;\n\n      if (res.data.ocr_limit === -1) {\n        ocrEl.textContent = t('unlimited');\n        ocrEl.classList.add('unlimited');\n        return;\n      }\n\n      if (ocrLeft <= 0) {\n        ocrEl.textContent = t('ocr_no_available');\n        ocrEl.classList.add('warning');\n        showUpgradeButton = true;\n      } else {\n        ocrEl.textContent = `${t('ocr_available')}: ${ocrLeft}`;\n        if (ocrLeft <= 1) ocrEl.classList.add('warning');\n      }\n\n      toggleUpgradeButton();\n    } catch (e) {\n      ocrEl.textContent = t('error');\n    }\n  }\n\n  function toggleUpgradeButton() {\n    upgradeBtnContainer.style.display = showUpgradeButton ? 'block' : 'none';\n  }\n\n  updateCreditNotice();\n  updateOCRNotice();\n\n  document.addEventListener('ttsUsageUpdated', updateCreditNotice);\n  document.addEventListener('ocrUsageUpdated', updateOCRNotice);\n});\n<\/script>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>","protected":false},"excerpt":{"rendered":"<p>OCR-Texterkennung &#038; \u00dcbersetzung \ud83d\udcf7 Text auf Bilder \u00fcbersetzen \ud83c\udf10 \ud83d\udcf7 Translate text from images \ud83c\udf10 \ud83d\udcf8 Foto aufnehmen \ud83d\udcf8 Take photo \ud83d\udcc1 Bild hochladen \ud83d\udcc1 Upload image \ud83c\udf0d Ziel-Sprache: \ud83c\udf0d Target language: \ud83c\udf10 \u00dcbersetzen \ud83c\udf10 Translate \u23f3 \u00dcbersetze&#8230; \u23f3 Translating&#8230; \ud83d\udd0a Vorlesen \ud83d\udd0a Read aloud \u2190 Zur\u00fcck \u2190 Back Erkannte Sprache: Detected language: Dialekt: Dialect:&hellip;&nbsp;<a href=\"https:\/\/textsnapper.com\/en\/translate\/\" rel=\"bookmark\">Read More \u00bb<span class=\"screen-reader-text\">translate<\/span><\/a><\/p>","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"page-templates\/template-pagebuilder-full-width.php","meta":{"neve_meta_sidebar":"","neve_meta_container":"","neve_meta_enable_content_width":"","neve_meta_content_width":0,"neve_meta_title_alignment":"","neve_meta_author_avatar":"","neve_post_elements_order":"","neve_meta_disable_header":"","neve_meta_disable_footer":"","neve_meta_disable_title":"","footnotes":""},"class_list":["post-7370","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.4 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>translate - TextSnapper<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/textsnapper.com\/en\/translate\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"translate - TextSnapper\" \/>\n<meta property=\"og:description\" content=\"OCR-Texterkennung &#038; \u00dcbersetzung \ud83d\udcf7 Text auf Bilder \u00fcbersetzen \ud83c\udf10 \ud83d\udcf7 Translate text from images \ud83c\udf10 \ud83d\udcf8 Foto aufnehmen \ud83d\udcf8 Take photo \ud83d\udcc1 Bild hochladen \ud83d\udcc1 Upload image \ud83c\udf0d Ziel-Sprache: \ud83c\udf0d Target language: \ud83c\udf10 \u00dcbersetzen \ud83c\udf10 Translate \u23f3 \u00dcbersetze&#8230; \u23f3 Translating&#8230; \ud83d\udd0a Vorlesen \ud83d\udd0a Read aloud \u2190 Zur\u00fcck \u2190 Back Erkannte Sprache: Detected language: Dialekt: Dialect:&hellip;&nbsp;Read More &raquo;translate\" \/>\n<meta property=\"og:url\" content=\"https:\/\/textsnapper.com\/en\/translate\/\" \/>\n<meta property=\"og:site_name\" content=\"TextSnapper\" \/>\n<meta property=\"article:modified_time\" content=\"2025-07-23T09:13:35+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"6 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/textsnapper.com\\\/translate\\\/\",\"url\":\"https:\\\/\\\/textsnapper.com\\\/translate\\\/\",\"name\":\"translate - TextSnapper\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/textsnapper.com\\\/#website\"},\"datePublished\":\"2025-04-21T07:02:55+00:00\",\"dateModified\":\"2025-07-23T09:13:35+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/textsnapper.com\\\/translate\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/textsnapper.com\\\/translate\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/textsnapper.com\\\/translate\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/textsnapper.com\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"translate\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/textsnapper.com\\\/#website\",\"url\":\"https:\\\/\\\/textsnapper.com\\\/\",\"name\":\"TextSnapper\",\"description\":\"Texte aus Bildern vorlesen, \u00fcbersetzen und einfach h\u00f6ren.\",\"publisher\":{\"@id\":\"https:\\\/\\\/textsnapper.com\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/textsnapper.com\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/textsnapper.com\\\/#organization\",\"name\":\"Textsnapper\",\"url\":\"https:\\\/\\\/textsnapper.com\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/textsnapper.com\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/textsnapper.com\\\/wp-content\\\/uploads\\\/2024\\\/12\\\/cropped-cropped-cropped-textsnapper.png\",\"contentUrl\":\"https:\\\/\\\/textsnapper.com\\\/wp-content\\\/uploads\\\/2024\\\/12\\\/cropped-cropped-cropped-textsnapper.png\",\"width\":200,\"height\":200,\"caption\":\"Textsnapper\"},\"image\":{\"@id\":\"https:\\\/\\\/textsnapper.com\\\/#\\\/schema\\\/logo\\\/image\\\/\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"translate - TextSnapper","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/textsnapper.com\/en\/translate\/","og_locale":"en_US","og_type":"article","og_title":"translate - TextSnapper","og_description":"OCR-Texterkennung &#038; \u00dcbersetzung \ud83d\udcf7 Text auf Bilder \u00fcbersetzen \ud83c\udf10 \ud83d\udcf7 Translate text from images \ud83c\udf10 \ud83d\udcf8 Foto aufnehmen \ud83d\udcf8 Take photo \ud83d\udcc1 Bild hochladen \ud83d\udcc1 Upload image \ud83c\udf0d Ziel-Sprache: \ud83c\udf0d Target language: \ud83c\udf10 \u00dcbersetzen \ud83c\udf10 Translate \u23f3 \u00dcbersetze&#8230; \u23f3 Translating&#8230; \ud83d\udd0a Vorlesen \ud83d\udd0a Read aloud \u2190 Zur\u00fcck \u2190 Back Erkannte Sprache: Detected language: Dialekt: Dialect:&hellip;&nbsp;Read More &raquo;translate","og_url":"https:\/\/textsnapper.com\/en\/translate\/","og_site_name":"TextSnapper","article_modified_time":"2025-07-23T09:13:35+00:00","twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"6 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/textsnapper.com\/translate\/","url":"https:\/\/textsnapper.com\/translate\/","name":"translate - TextSnapper","isPartOf":{"@id":"https:\/\/textsnapper.com\/#website"},"datePublished":"2025-04-21T07:02:55+00:00","dateModified":"2025-07-23T09:13:35+00:00","breadcrumb":{"@id":"https:\/\/textsnapper.com\/translate\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/textsnapper.com\/translate\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/textsnapper.com\/translate\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/textsnapper.com\/"},{"@type":"ListItem","position":2,"name":"translate"}]},{"@type":"WebSite","@id":"https:\/\/textsnapper.com\/#website","url":"https:\/\/textsnapper.com\/","name":"TextSnapper","description":"Texte aus Bildern vorlesen, \u00fcbersetzen und einfach h\u00f6ren.","publisher":{"@id":"https:\/\/textsnapper.com\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/textsnapper.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/textsnapper.com\/#organization","name":"Textsnapper","url":"https:\/\/textsnapper.com\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/textsnapper.com\/#\/schema\/logo\/image\/","url":"https:\/\/textsnapper.com\/wp-content\/uploads\/2024\/12\/cropped-cropped-cropped-textsnapper.png","contentUrl":"https:\/\/textsnapper.com\/wp-content\/uploads\/2024\/12\/cropped-cropped-cropped-textsnapper.png","width":200,"height":200,"caption":"Textsnapper"},"image":{"@id":"https:\/\/textsnapper.com\/#\/schema\/logo\/image\/"}}]}},"_links":{"self":[{"href":"https:\/\/textsnapper.com\/en\/wp-json\/wp\/v2\/pages\/7370","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/textsnapper.com\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/textsnapper.com\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/textsnapper.com\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/textsnapper.com\/en\/wp-json\/wp\/v2\/comments?post=7370"}],"version-history":[{"count":395,"href":"https:\/\/textsnapper.com\/en\/wp-json\/wp\/v2\/pages\/7370\/revisions"}],"predecessor-version":[{"id":10704,"href":"https:\/\/textsnapper.com\/en\/wp-json\/wp\/v2\/pages\/7370\/revisions\/10704"}],"wp:attachment":[{"href":"https:\/\/textsnapper.com\/en\/wp-json\/wp\/v2\/media?parent=7370"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}