Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta | |
| name="viewport" | |
| content="width=device-width, initial-scale=1.0" | |
| /> | |
| <meta | |
| name="description" | |
| content="Free Urdu, Hindi, Roman Urdu and Hinglish to English translator." | |
| /> | |
| <title>Urdu & Hindi to English Translator</title> | |
| <style> | |
| :root { | |
| --primary: #3157f6; | |
| --primary-dark: #2446cc; | |
| --secondary: #7b4dff; | |
| --background: #f4f7fb; | |
| --card: #ffffff; | |
| --text: #172033; | |
| --muted: #667085; | |
| --border: #dce3ef; | |
| --success: #14804a; | |
| --success-bg: #ecfdf3; | |
| --error: #b42318; | |
| --error-bg: #fff4f2; | |
| --warning-bg: #fffaeb; | |
| --warning-border: #fedf89; | |
| --warning-text: #7a4d00; | |
| --shadow: 0 18px 60px rgba(35, 55, 95, 0.12); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| margin: 0; | |
| min-height: 100vh; | |
| background: | |
| radial-gradient(circle at 10% 0%, #dfe8ff, transparent 34%), | |
| var(--background); | |
| color: var(--text); | |
| font-family: | |
| Inter, | |
| -apple-system, | |
| BlinkMacSystemFont, | |
| "Segoe UI", | |
| sans-serif; | |
| } | |
| button, | |
| textarea, | |
| select, | |
| input { | |
| font: inherit; | |
| } | |
| main { | |
| width: min(1120px, calc(100% - 28px)); | |
| margin: 28px auto; | |
| } | |
| .header { | |
| display: flex; | |
| align-items: center; | |
| gap: 14px; | |
| margin-bottom: 20px; | |
| } | |
| .logo { | |
| width: 52px; | |
| height: 52px; | |
| display: grid; | |
| place-items: center; | |
| flex-shrink: 0; | |
| border-radius: 16px; | |
| background: linear-gradient( | |
| 135deg, | |
| var(--primary), | |
| var(--secondary) | |
| ); | |
| color: #ffffff; | |
| font-size: 25px; | |
| font-weight: 800; | |
| box-shadow: 0 12px 30px rgba(49, 87, 246, 0.25); | |
| } | |
| h1 { | |
| margin: 0; | |
| font-size: clamp(1.55rem, 3vw, 2.3rem); | |
| line-height: 1.15; | |
| } | |
| .subtitle { | |
| margin-top: 5px; | |
| color: var(--muted); | |
| } | |
| .card { | |
| padding: 22px; | |
| background: var(--card); | |
| border: 1px solid var(--border); | |
| border-radius: 24px; | |
| box-shadow: var(--shadow); | |
| } | |
| .translator-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 18px; | |
| } | |
| .field { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 9px; | |
| } | |
| .field-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 12px; | |
| } | |
| label { | |
| font-weight: 800; | |
| } | |
| .small-label { | |
| color: var(--muted); | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| } | |
| textarea, | |
| select { | |
| width: 100%; | |
| color: var(--text); | |
| background: #ffffff; | |
| border: 1px solid var(--border); | |
| outline: none; | |
| transition: | |
| border-color 0.2s ease, | |
| box-shadow 0.2s ease; | |
| } | |
| textarea { | |
| min-height: 320px; | |
| padding: 16px; | |
| border-radius: 16px; | |
| resize: vertical; | |
| line-height: 1.65; | |
| } | |
| select { | |
| width: auto; | |
| min-width: 155px; | |
| padding: 11px 13px; | |
| border-radius: 12px; | |
| } | |
| textarea:focus, | |
| select:focus { | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 4px rgba(49, 87, 246, 0.1); | |
| } | |
| textarea[readonly] { | |
| background: #fbfcff; | |
| } | |
| .controls { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 9px; | |
| margin-top: 12px; | |
| } | |
| button, | |
| .file-button { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 7px; | |
| min-height: 42px; | |
| padding: 10px 15px; | |
| border: 0; | |
| border-radius: 12px; | |
| font-weight: 800; | |
| text-decoration: none; | |
| cursor: pointer; | |
| transition: | |
| transform 0.15s ease, | |
| background-color 0.15s ease, | |
| opacity 0.15s ease; | |
| } | |
| button:hover, | |
| .file-button:hover { | |
| transform: translateY(-1px); | |
| } | |
| button:disabled { | |
| opacity: 0.55; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .primary-button { | |
| color: #ffffff; | |
| background: var(--primary); | |
| } | |
| .primary-button:hover { | |
| background: var(--primary-dark); | |
| } | |
| .secondary-button, | |
| .file-button { | |
| color: #344054; | |
| background: #eef2f8; | |
| } | |
| .danger-button { | |
| color: var(--error); | |
| background: #fff0ee; | |
| } | |
| input[type="file"] { | |
| display: none; | |
| } | |
| .metadata { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| margin-top: 15px; | |
| } | |
| .pill { | |
| padding: 7px 11px; | |
| color: #475467; | |
| background: #f2f4f7; | |
| border-radius: 999px; | |
| font-size: 0.84rem; | |
| font-weight: 700; | |
| } | |
| .status { | |
| margin-top: 16px; | |
| padding: 12px 14px; | |
| color: var(--muted); | |
| background: #f8fafc; | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| line-height: 1.5; | |
| } | |
| .status.success { | |
| color: var(--success); | |
| background: var(--success-bg); | |
| border-color: #abefc6; | |
| } | |
| .status.error { | |
| color: var(--error); | |
| background: var(--error-bg); | |
| border-color: #fecdca; | |
| } | |
| .microphone-section { | |
| margin-top: 19px; | |
| padding-top: 19px; | |
| border-top: 1px solid var(--border); | |
| } | |
| .microphone-section h2 { | |
| margin: 0 0 6px; | |
| font-size: 1.05rem; | |
| } | |
| .microphone-section p { | |
| margin: 0; | |
| color: var(--muted); | |
| line-height: 1.5; | |
| } | |
| .notice { | |
| margin-top: 18px; | |
| padding: 14px; | |
| color: var(--warning-text); | |
| background: var(--warning-bg); | |
| border: 1px solid var(--warning-border); | |
| border-radius: 13px; | |
| line-height: 1.55; | |
| } | |
| .footer { | |
| margin-top: 18px; | |
| color: var(--muted); | |
| text-align: center; | |
| font-size: 0.88rem; | |
| } | |
| @media (max-width: 800px) { | |
| .translator-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| textarea { | |
| min-height: 230px; | |
| } | |
| .card { | |
| padding: 15px; | |
| border-radius: 18px; | |
| } | |
| .field-header { | |
| align-items: flex-start; | |
| flex-direction: column; | |
| } | |
| select { | |
| width: 100%; | |
| } | |
| } | |
| @media (max-width: 480px) { | |
| main { | |
| width: min(100% - 18px, 1120px); | |
| margin: 16px auto; | |
| } | |
| .header { | |
| align-items: flex-start; | |
| } | |
| .logo { | |
| width: 46px; | |
| height: 46px; | |
| } | |
| button, | |
| .file-button { | |
| width: 100%; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <main> | |
| <header class="header"> | |
| <div class="logo" aria-hidden="true">文</div> | |
| <div> | |
| <h1>AI English Translator</h1> | |
| <div class="subtitle"> | |
| Urdu · Hindi · Roman Urdu · Hinglish → English | |
| </div> | |
| </div> | |
| </header> | |
| <section class="card"> | |
| <div class="translator-grid"> | |
| <div class="field"> | |
| <div class="field-header"> | |
| <label for="sourceText">Original text</label> | |
| <span class="small-label"> | |
| Enter or dictate your message | |
| </span> | |
| </div> | |
| <textarea | |
| id="sourceText" | |
| dir="auto" | |
| maxlength="5000" | |
| placeholder="اردو، हिंदी, Roman Urdu ya Hinglish yahan likhein..." | |
| ></textarea> | |
| <div class="controls"> | |
| <select | |
| id="sourceLanguage" | |
| aria-label="Select source language" | |
| > | |
| <option value="auto">Auto detect</option> | |
| <option value="ur">Urdu</option> | |
| <option value="hi">Hindi</option> | |
| </select> | |
| <label class="file-button" for="textFile"> | |
| Import TXT | |
| </label> | |
| <input | |
| id="textFile" | |
| type="file" | |
| accept=".txt,text/plain" | |
| /> | |
| </div> | |
| </div> | |
| <div class="field"> | |
| <div class="field-header"> | |
| <label for="translatedText"> | |
| English translation | |
| </label> | |
| <span class="small-label"> | |
| Generated translation | |
| </span> | |
| </div> | |
| <textarea | |
| id="translatedText" | |
| readonly | |
| placeholder="Your English translation will appear here..." | |
| ></textarea> | |
| <div class="controls"> | |
| <button | |
| id="translateButton" | |
| class="primary-button" | |
| type="button" | |
| > | |
| Translate | |
| </button> | |
| <button | |
| id="copyButton" | |
| class="secondary-button" | |
| type="button" | |
| > | |
| Copy | |
| </button> | |
| <button | |
| id="downloadButton" | |
| class="secondary-button" | |
| type="button" | |
| > | |
| Download | |
| </button> | |
| <button | |
| id="clearButton" | |
| class="danger-button" | |
| type="button" | |
| > | |
| Clear | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="metadata"> | |
| <span id="detectedLanguage" class="pill"> | |
| Detected: — | |
| </span> | |
| <span id="characterCount" class="pill"> | |
| 0 characters | |
| </span> | |
| </div> | |
| <div | |
| id="statusMessage" | |
| class="status" | |
| role="status" | |
| aria-live="polite" | |
| > | |
| Ready. | |
| </div> | |
| <section class="microphone-section"> | |
| <h2>Browser microphone dictation</h2> | |
| <p> | |
| Speak into your microphone and the browser will add the | |
| transcript to the original text box. | |
| </p> | |
| <div class="controls"> | |
| <select | |
| id="speechLanguage" | |
| aria-label="Select microphone language" | |
| > | |
| <option value="ur-PK">Urdu — Pakistan</option> | |
| <option value="hi-IN">Hindi — India</option> | |
| <option value="en-IN">Hinglish — India</option> | |
| </select> | |
| <button | |
| id="startMicrophoneButton" | |
| class="primary-button" | |
| type="button" | |
| > | |
| Start microphone | |
| </button> | |
| <button | |
| id="stopMicrophoneButton" | |
| class="danger-button" | |
| type="button" | |
| disabled | |
| > | |
| Stop microphone | |
| </button> | |
| </div> | |
| </section> | |
| <div class="notice"> | |
| <strong>Static Space edition:</strong> | |
| this version supports browser-based text translation, | |
| microphone dictation and TXT imports. Audio files, videos, | |
| PDFs, DOCX files and image OCR require the full Python | |
| application. Translation is sent to a third-party public | |
| translation service, so do not enter confidential information. | |
| </div> | |
| </section> | |
| <footer class="footer"> | |
| Free browser-based Urdu, Hindi, Roman Urdu and Hinglish | |
| translator. | |
| </footer> | |
| </main> | |
| <script> | |
| ; | |
| const sourceText = document.getElementById("sourceText"); | |
| const translatedText = | |
| document.getElementById("translatedText"); | |
| const sourceLanguage = | |
| document.getElementById("sourceLanguage"); | |
| const speechLanguage = | |
| document.getElementById("speechLanguage"); | |
| const textFile = document.getElementById("textFile"); | |
| const translateButton = | |
| document.getElementById("translateButton"); | |
| const copyButton = | |
| document.getElementById("copyButton"); | |
| const downloadButton = | |
| document.getElementById("downloadButton"); | |
| const clearButton = | |
| document.getElementById("clearButton"); | |
| const startMicrophoneButton = | |
| document.getElementById("startMicrophoneButton"); | |
| const stopMicrophoneButton = | |
| document.getElementById("stopMicrophoneButton"); | |
| const detectedLanguage = | |
| document.getElementById("detectedLanguage"); | |
| const characterCount = | |
| document.getElementById("characterCount"); | |
| const statusMessage = | |
| document.getElementById("statusMessage"); | |
| const MAX_FILE_SIZE = 1024 * 1024; | |
| const MAX_TRANSLATION_LENGTH = 5000; | |
| const CHUNK_LENGTH = 450; | |
| function updateStatus(message, type = "") { | |
| statusMessage.textContent = message; | |
| statusMessage.className = "status"; | |
| if (type === "success") { | |
| statusMessage.classList.add("success"); | |
| } | |
| if (type === "error") { | |
| statusMessage.classList.add("error"); | |
| } | |
| } | |
| function updateCharacterCount() { | |
| const count = sourceText.value.length; | |
| characterCount.textContent = | |
| `${count.toLocaleString()} characters`; | |
| } | |
| function detectInputLanguage(text) { | |
| const trimmedText = text.trim(); | |
| if (!trimmedText) { | |
| return { | |
| code: "ur", | |
| label: "Unknown" | |
| }; | |
| } | |
| const urduCharacters = | |
| trimmedText.match(/[\u0600-\u06FF]/g) || []; | |
| const hindiCharacters = | |
| trimmedText.match(/[\u0900-\u097F]/g) || []; | |
| if (urduCharacters.length > hindiCharacters.length) { | |
| return { | |
| code: "ur", | |
| label: "Urdu" | |
| }; | |
| } | |
| if (hindiCharacters.length > urduCharacters.length) { | |
| return { | |
| code: "hi", | |
| label: "Hindi" | |
| }; | |
| } | |
| const romanUrduWords = [ | |
| "aap", | |
| "ap", | |
| "main", | |
| "mein", | |
| "mujhe", | |
| "mera", | |
| "meri", | |
| "mere", | |
| "hum", | |
| "ham", | |
| "hai", | |
| "hain", | |
| "ho", | |
| "tha", | |
| "thi", | |
| "nahi", | |
| "nahin", | |
| "kya", | |
| "kyun", | |
| "kaise", | |
| "kab", | |
| "kahan", | |
| "karna", | |
| "karo", | |
| "karen", | |
| "chahiye", | |
| "acha", | |
| "achha", | |
| "theek", | |
| "lekin", | |
| "aur", | |
| "wala", | |
| "wali", | |
| "liye", | |
| "bohat", | |
| "bahut" | |
| ]; | |
| const normalizedWords = trimmedText | |
| .toLowerCase() | |
| .replace(/[^\p{L}\p{N}\s]/gu, " ") | |
| .split(/\s+/) | |
| .filter(Boolean); | |
| const romanMatchCount = normalizedWords.filter( | |
| (word) => romanUrduWords.includes(word) | |
| ).length; | |
| if (romanMatchCount >= 1) { | |
| return { | |
| code: "ur", | |
| label: "Roman Urdu / Hinglish" | |
| }; | |
| } | |
| return { | |
| code: "ur", | |
| label: "Roman or unknown" | |
| }; | |
| } | |
| function splitTextIntoChunks(text, maximumLength) { | |
| const chunks = []; | |
| let remainingText = text.trim(); | |
| while (remainingText.length > maximumLength) { | |
| let splitPosition = | |
| remainingText.lastIndexOf(" ", maximumLength); | |
| if (splitPosition < maximumLength * 0.5) { | |
| splitPosition = maximumLength; | |
| } | |
| chunks.push( | |
| remainingText.slice(0, splitPosition).trim() | |
| ); | |
| remainingText = | |
| remainingText.slice(splitPosition).trim(); | |
| } | |
| if (remainingText) { | |
| chunks.push(remainingText); | |
| } | |
| return chunks; | |
| } | |
| function decodeHtmlEntities(value) { | |
| const textArea = document.createElement("textarea"); | |
| textArea.innerHTML = value; | |
| return textArea.value; | |
| } | |
| async function translateChunk(text, languageCode) { | |
| const endpoint = | |
| "https://api.mymemory.translated.net/get"; | |
| const parameters = new URLSearchParams({ | |
| q: text, | |
| langpair: `${languageCode}|en` | |
| }); | |
| const response = await fetch( | |
| `${endpoint}?${parameters.toString()}` | |
| ); | |
| if (!response.ok) { | |
| throw new Error( | |
| `Translation service returned status ${response.status}.` | |
| ); | |
| } | |
| const data = await response.json(); | |
| const translation = | |
| data?.responseData?.translatedText; | |
| if (!translation) { | |
| throw new Error( | |
| data?.responseDetails || | |
| "The translation service returned no text." | |
| ); | |
| } | |
| return decodeHtmlEntities(translation); | |
| } | |
| async function translateText() { | |
| const originalText = sourceText.value.trim(); | |
| if (!originalText) { | |
| updateStatus( | |
| "Please enter or dictate some text first.", | |
| "error" | |
| ); | |
| sourceText.focus(); | |
| return; | |
| } | |
| if (originalText.length > MAX_TRANSLATION_LENGTH) { | |
| updateStatus( | |
| `Please limit the text to ${MAX_TRANSLATION_LENGTH.toLocaleString()} characters.`, | |
| "error" | |
| ); | |
| return; | |
| } | |
| const detected = detectInputLanguage(originalText); | |
| const selectedLanguage = | |
| sourceLanguage.value === "auto" | |
| ? detected.code | |
| : sourceLanguage.value; | |
| detectedLanguage.textContent = | |
| `Detected: ${detected.label}`; | |
| translateButton.disabled = true; | |
| translatedText.value = ""; | |
| updateStatus("Translating your text..."); | |
| try { | |
| const chunks = splitTextIntoChunks( | |
| originalText, | |
| CHUNK_LENGTH | |
| ); | |
| const completedTranslations = []; | |
| for (let index = 0; index < chunks.length; index += 1) { | |
| updateStatus( | |
| `Translating section ${index + 1} of ${chunks.length}...` | |
| ); | |
| const translation = await translateChunk( | |
| chunks[index], | |
| selectedLanguage | |
| ); | |
| completedTranslations.push(translation); | |
| } | |
| translatedText.value = | |
| completedTranslations | |
| .join(" ") | |
| .replace(/\s+/g, " ") | |
| .trim(); | |
| updateStatus( | |
| "Translation completed successfully.", | |
| "success" | |
| ); | |
| } catch (error) { | |
| console.error(error); | |
| updateStatus( | |
| `Translation failed: ${error.message}`, | |
| "error" | |
| ); | |
| } finally { | |
| translateButton.disabled = false; | |
| } | |
| } | |
| async function copyTranslation() { | |
| const value = translatedText.value.trim(); | |
| if (!value) { | |
| updateStatus( | |
| "There is no translation to copy.", | |
| "error" | |
| ); | |
| return; | |
| } | |
| try { | |
| await navigator.clipboard.writeText(value); | |
| updateStatus( | |
| "Translation copied to the clipboard.", | |
| "success" | |
| ); | |
| } catch (error) { | |
| translatedText.select(); | |
| const copied = document.execCommand("copy"); | |
| if (copied) { | |
| updateStatus( | |
| "Translation copied to the clipboard.", | |
| "success" | |
| ); | |
| } else { | |
| updateStatus( | |
| "The browser could not copy the translation.", | |
| "error" | |
| ); | |
| } | |
| } | |
| } | |
| function downloadTranslation() { | |
| const value = translatedText.value.trim(); | |
| if (!value) { | |
| updateStatus( | |
| "There is no translation to download.", | |
| "error" | |
| ); | |
| return; | |
| } | |
| const blob = new Blob( | |
| [value], | |
| { | |
| type: "text/plain;charset=utf-8" | |
| } | |
| ); | |
| const objectUrl = URL.createObjectURL(blob); | |
| const downloadLink = document.createElement("a"); | |
| downloadLink.href = objectUrl; | |
| downloadLink.download = "english_translation.txt"; | |
| document.body.appendChild(downloadLink); | |
| downloadLink.click(); | |
| downloadLink.remove(); | |
| URL.revokeObjectURL(objectUrl); | |
| updateStatus( | |
| "Translation downloaded.", | |
| "success" | |
| ); | |
| } | |
| function clearApplication() { | |
| sourceText.value = ""; | |
| translatedText.value = ""; | |
| textFile.value = ""; | |
| detectedLanguage.textContent = "Detected: —"; | |
| updateCharacterCount(); | |
| updateStatus("Cleared."); | |
| sourceText.focus(); | |
| } | |
| async function importTextFile(event) { | |
| const file = event.target.files?.[0]; | |
| if (!file) { | |
| return; | |
| } | |
| if (file.size > MAX_FILE_SIZE) { | |
| updateStatus( | |
| "The TXT file is too large. Maximum size is 1 MB.", | |
| "error" | |
| ); | |
| textFile.value = ""; | |
| return; | |
| } | |
| const fileName = file.name.toLowerCase(); | |
| if ( | |
| file.type !== "text/plain" && | |
| !fileName.endsWith(".txt") | |
| ) { | |
| updateStatus( | |
| "Please upload a valid TXT file.", | |
| "error" | |
| ); | |
| textFile.value = ""; | |
| return; | |
| } | |
| try { | |
| const content = await file.text(); | |
| if (!content.trim()) { | |
| throw new Error("The selected TXT file is empty."); | |
| } | |
| sourceText.value = | |
| content.slice(0, MAX_TRANSLATION_LENGTH); | |
| updateCharacterCount(); | |
| const detected = detectInputLanguage( | |
| sourceText.value | |
| ); | |
| detectedLanguage.textContent = | |
| `Detected: ${detected.label}`; | |
| updateStatus( | |
| "TXT file loaded successfully.", | |
| "success" | |
| ); | |
| } catch (error) { | |
| updateStatus( | |
| `Could not read the file: ${error.message}`, | |
| "error" | |
| ); | |
| } | |
| } | |
| sourceText.addEventListener( | |
| "input", | |
| updateCharacterCount | |
| ); | |
| sourceText.addEventListener( | |
| "keydown", | |
| (event) => { | |
| if ( | |
| (event.ctrlKey || event.metaKey) && | |
| event.key === "Enter" | |
| ) { | |
| translateText(); | |
| } | |
| } | |
| ); | |
| translateButton.addEventListener( | |
| "click", | |
| translateText | |
| ); | |
| copyButton.addEventListener( | |
| "click", | |
| copyTranslation | |
| ); | |
| downloadButton.addEventListener( | |
| "click", | |
| downloadTranslation | |
| ); | |
| clearButton.addEventListener( | |
| "click", | |
| clearApplication | |
| ); | |
| textFile.addEventListener( | |
| "change", | |
| importTextFile | |
| ); | |
| const SpeechRecognition = | |
| window.SpeechRecognition || | |
| window.webkitSpeechRecognition; | |
| let speechRecognition = null; | |
| if (SpeechRecognition) { | |
| speechRecognition = new SpeechRecognition(); | |
| speechRecognition.continuous = true; | |
| speechRecognition.interimResults = true; | |
| let finalTranscript = ""; | |
| speechRecognition.addEventListener( | |
| "start", | |
| () => { | |
| finalTranscript = ""; | |
| startMicrophoneButton.disabled = true; | |
| stopMicrophoneButton.disabled = false; | |
| updateStatus( | |
| "Listening through the microphone..." | |
| ); | |
| } | |
| ); | |
| speechRecognition.addEventListener( | |
| "result", | |
| (event) => { | |
| let interimTranscript = ""; | |
| for ( | |
| let index = event.resultIndex; | |
| index < event.results.length; | |
| index += 1 | |
| ) { | |
| const transcript = | |
| event.results[index][0].transcript; | |
| if (event.results[index].isFinal) { | |
| finalTranscript += `${transcript} `; | |
| } else { | |
| interimTranscript += transcript; | |
| } | |
| } | |
| if (finalTranscript.trim()) { | |
| const existingText = | |
| sourceText.value.trim(); | |
| sourceText.value = | |
| `${existingText}${existingText ? " " : ""}${finalTranscript.trim()}`; | |
| finalTranscript = ""; | |
| updateCharacterCount(); | |
| } | |
| if (interimTranscript) { | |
| updateStatus( | |
| `Listening: ${interimTranscript}` | |
| ); | |
| } | |
| } | |
| ); | |
| speechRecognition.addEventListener( | |
| "error", | |
| (event) => { | |
| updateStatus( | |
| `Microphone error: ${event.error}`, | |
| "error" | |
| ); | |
| } | |
| ); | |
| speechRecognition.addEventListener( | |
| "end", | |
| () => { | |
| startMicrophoneButton.disabled = false; | |
| stopMicrophoneButton.disabled = true; | |
| updateStatus("Microphone stopped."); | |
| } | |
| ); | |
| startMicrophoneButton.addEventListener( | |
| "click", | |
| () => { | |
| try { | |
| speechRecognition.lang = | |
| speechLanguage.value; | |
| speechRecognition.start(); | |
| } catch (error) { | |
| updateStatus( | |
| `Could not start microphone: ${error.message}`, | |
| "error" | |
| ); | |
| } | |
| } | |
| ); | |
| stopMicrophoneButton.addEventListener( | |
| "click", | |
| () => { | |
| speechRecognition.stop(); | |
| } | |
| ); | |
| } else { | |
| startMicrophoneButton.disabled = true; | |
| stopMicrophoneButton.disabled = true; | |
| updateStatus( | |
| "Microphone dictation is not supported by this browser. Text translation is still available." | |
| ); | |
| } | |
| updateCharacterCount(); | |
| </script> | |
| </body> | |
| </html> |