Help
RSS
API
Feed
Maltego
Contact
Domain > api.starvault.dev
×
More information on this domain is in
AlienVault OTX
Is this malicious?
Yes
No
DNS Resolutions
Date
IP Address
2026-01-04
104.21.33.120
(
ClassC
)
2026-02-10
172.67.162.136
(
ClassC
)
Port 80
HTTP/1.1 200 OKDate: Tue, 10 Feb 2026 04:02:14 GMTContent-Type: text/html; charsetUTF-8Content-Length: 24735Connection: keep-aliveCf-Placement: local-PDXReport-To: {group:cf-nel,max_age:604800,endpoints:{url:https://a.nel.cloudflare.com/report/v4?sNAy6%2BheyUQjdM7DP9BgdizfnNI4YqfOyCeEeMBYNd5LIAD%2BiVemXmqDBKSJbLjisx3JyXVU%2F9f%2FqDL79wYxkSRaMWMHRcgLtzGy%2BS6rXEg%3D%3D}}Nel: {report_to:cf-nel,success_fraction:0.0,max_age:604800}Server: cloudflareCF-RAY: 9cb8b5b96e01ef3f-PDXalt-svc: h3:443; ma86400 !DOCTYPE html>html langen>head> meta charsetUTF-8> meta nameviewport contentwidthdevice-width, initial-scale1.0> title>SC2 Campaign Translator/title> script srchttps://cdn.tailwindcss.com>/script> style> select { appearance: none; background-image: url(data:image/svg+xml;charsetUTF-8,%3csvg xmlnshttp://www.w3.org/2000/svg viewBox0 0 24 24 fillnone strokewhite stroke-width2 stroke-linecapround stroke-linejoinround%3e%3cpolyline points6 9 12 15 18 9%3e%3c/polyline%3e%3c/svg%3e); background-repeat: no-repeat; background-position: right 0.5rem center; background-size: 1.25em; padding-right: 2rem !important; } select:disabled { opacity: 0.8; -webkit-text-fill-color: white; /* Fix for Safari/Mac disabled text color */ color: white !important; } /style> script> tailwind.config { theme: { extend: { colors: { terran: #4A90E2, zerg: #E25C4A, protoss: #F5A623, } } } } /script>/head>body classbg-gray-900 text-white min-h-screen> div classcontainer mx-auto px-4 py-8 max-w-2xl> !-- Header --> header classtext-center mb-8> h1 classtext-3xl font-bold text-terran mb-2>Moukas Campaign Translator/h1> /header> !-- Upload Section --> section idupload-section classbg-gray-800 rounded-lg p-6> !-- Locale Selectors --> div classflex flex-col items-center mb-8 gap-2> div classflex items-center justify-center gap-6 p-4 bg-gray-900/50 rounded-xl border border-gray-700 w-full> div classflex items-center gap-3 titleCurrently only Chinese (Simplified) source is supported> label classtext-xs font-semibold text-gray-500 uppercase tracking-wider>From/label> select idsource-locale disabled classbg-gray-800 text-white text-sm rounded-lg px-3 py-2 border border-gray-600 cursor-not-allowed appearance-none min-w-180px titleCurrently only Chinese (Simplified) source is supported> option valuezhCN.SC2Data>π¨π³ Chinese (Simplified)/option> option valuekoKR.SC2Data>π°π· Korean/option> option valueenUS.SC2Data>πΊπΈ English/option> option valuedeDE.SC2Data>π©πͺ German/option> option valueesES.SC2Data>πͺπΈ Spanish/option> option valuefrFR.SC2Data>π«π· French/option> /select> /div> div classflex items-center> svg classw-5 h-5 text-gray-600 fillnone strokecurrentColor viewBox0 0 24 24> path stroke-linecapround stroke-linejoinround stroke-width2 dM13 7l5 5m0 0l-5 5m5-5H6 /> /svg> /div> div classflex items-center gap-3 titleCurrently only English target is supported> label classtext-xs font-semibold text-gray-500 uppercase tracking-wider>To/label> select idtarget-locale disabled classbg-gray-800 text-white text-sm rounded-lg px-3 py-2 border border-gray-600 cursor-not-allowed appearance-none min-w-180px titleCurrently only English target is supported> option valueenUS.SC2Data>πΊπΈ English/option> option valuekoKR.SC2Data>π°π· Korean/option> option valuezhCN.SC2Data>π¨π³ Chinese (Simplified)/option> option valuedeDE.SC2Data>π©πͺ German/option> option valueesES.SC2Data>πͺπΈ Spanish/option> option valuefrFR.SC2Data>π«π· French/option> /select> /div> /div> p classtext-10px text-gray-500 flex items-center gap-1> svg classw-3 h-3 fillnone strokecurrentColor viewBox0 0 24 24>path stroke-linecapround stroke-linejoinround stroke-width2 dM13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z />/svg> Currently only Chinese to English translation is supported /p> /div> div iddrop-zone classdrop-zone border-2 border-dashed border-gray-600 rounded-lg p-8 text-center cursor-pointer hover:border-terran transition> svg classw-16 h-16 mx-auto mb-4 text-gray-500 fillnone strokecurrentColor viewBox0 0 24 24> path stroke-linecapround stroke-linejoinround stroke-width2 dM7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12 /> /svg> p classtext-lg mb-2>Drag & drop your SC2 archive here/p> p classtext-sm text-gray-500>or click to browse (.zip, .SC2Mod, .SC2Map)/p> input typefile idfile-input classhidden accept.zip,.sc2mod,.sc2map,.SC2Mod,.SC2Map> /div> !-- File Info --> div idfile-info classhidden mt-4 p-4 bg-gray-700 rounded> div classflex justify-between items-center> div> p classfont-medium idfile-name>filename.zip/p> p classtext-sm text-gray-400 idfile-size>0 MB/p> /div> button idstart-upload classpx-4 py-2 bg-terran text-white rounded hover:bg-blue-600 transition>Start Translation/button> /div> !-- Options --> div classmt-3 pt-3 border-t border-gray-600> label classflex items-center gap-2 cursor-pointer group> input typecheckbox idmerge-with-existing classw-4 h-4 rounded bg-gray-800 border-gray-500 text-terran focus:ring-terran focus:ring-offset-gray-700> span classtext-sm text-gray-300 group-hover:text-white transition>Merge with existing target locale entries/span> span classrelative> svg classw-4 h-4 text-gray-500 hover:text-gray-300 cursor-help fillnone strokecurrentColor viewBox0 0 24 24> path stroke-linecapround stroke-linejoinround stroke-width2 dM13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z /> /svg> span classabsolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 text-xs text-white bg-gray-900 rounded shadow-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10> Enable this if you see missing values in-game like Param/Value/....br> This merges existing English strings with the translated ones. /span> /span> /label> /div> /div> !-- Progress Section --> div idprogress-section classhidden mt-4> div classflex justify-between mb-2> span idprogress-label classtext-sm>Uploading.../span> span idprogress-percent classtext-sm text-terran>0%/span> /div> div classw-full bg-gray-700 rounded-full h-2> div idprogress-bar classbg-terran h-2 rounded-full transition-all duration-300 stylewidth: 0%>/div> /div> !-- Status Messages --> div idstatus-details classmt-4 space-y-2 text-sm text-gray-400> !-- Dynamic status items --> /div> /div> !-- Complete Section --> div idcomplete-section classhidden mt-4 p-4 bg-green-900/50 rounded text-center> svg classw-16 h-16 mx-auto mb-4 text-green-500 fillnone strokecurrentColor viewBox0 0 24 24> path stroke-linecapround stroke-linejoinround stroke-width2 dM5 13l4 4L19 7 /> /svg> p classtext-xl mb-4 text-green-400>Translation Complete!/p> div classflex gap-4 justify-center mb-4> button iddownload-btn classpx-4 py-2 bg-terran text-white rounded hover:bg-blue-600 transition>Download Translated Campaign/button> button idpreserve-btn classpx-4 py-2 bg-protoss text-white rounded hover:bg-yellow-600 transition>Preserve Permanently/button> /div> !-- Download URL --> div classmt-4 p-3 bg-gray-800 rounded text-left> p classtext-xs text-gray-500 mb-1>Download URL:/p> div classflex items-center gap-2> code iddownload-url classflex-1 text-xs text-terran break-all bg-gray-900 px-2 py-1 rounded>/code> button idcopy-url-btn classtext-xs text-gray-400 hover:text-white transition titleCopy URL> svg classw-4 h-4 fillnone strokecurrentColor viewBox0 0 24 24> path stroke-linecapround stroke-linejoinround stroke-width2 dM8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z /> /svg> /button> /div> /div> /div> !-- Error Section --> div iderror-section classhidden mt-4 p-4 bg-red-900/50 rounded> p classfont-medium text-red-400>Error:/p> p iderror-message classtext-sm text-gray-300 mt-1>/p> p classtext-xs text-gray-500 mt-2>This is an alpha release. You may retry later./p> /div> /section> !-- Recent Translations --> section idrecent-section classmt-8> h2 classtext-xl font-bold mb-4>Recent Translations/h2> div idrecent-list classspace-y-2> p classtext-gray-500 text-sm>No recent translations/p> /div> /section> /div> script> // API base URL const API_BASE /api; const DIRECT_UPLOAD_LIMIT 100 * 1024 * 1024; // 100MB const CHUNK_SIZE 20 * 1024 * 1024; // 20MB // State let state { file: null, instanceId: null, uploadId: null, status: idle, preserved: false, preservedOutputKey: null }; // Track shown status messages to avoid duplicates const shownMessages new Set(); // DOM Elements const dropZone document.getElementById(drop-zone); const fileInput document.getElementById(file-input); const fileInfo document.getElementById(file-info); const fileName document.getElementById(file-name); const fileSize document.getElementById(file-size); const startUploadBtn document.getElementById(start-upload); const progressSection document.getElementById(progress-section); const progressBar document.getElementById(progress-bar); const progressPercent document.getElementById(progress-percent); const progressLabel document.getElementById(progress-label); const statusDetails document.getElementById(status-details); const completeSection document.getElementById(complete-section); const errorSection document.getElementById(error-section); const errorMessage document.getElementById(error-message); const downloadBtn document.getElementById(download-btn); const preserveBtn document.getElementById(preserve-btn); const recentList document.getElementById(recent-list); const downloadUrlEl document.getElementById(download-url); const copyUrlBtn document.getElementById(copy-url-btn); const mergeWithExistingCheckbox document.getElementById(merge-with-existing); // Event Listeners dropZone.addEventListener(click, () > fileInput.click()); dropZone.addEventListener(dragover, (e) > { e.preventDefault(); dropZone.classList.add(border-terran, bg-gray-700); }); dropZone.addEventListener(dragleave, () > { dropZone.classList.remove(border-terran, bg-gray-700); }); dropZone.addEventListener(drop, (e) > { e.preventDefault(); dropZone.classList.remove(border-terran, bg-gray-700); if (e.dataTransfer.files.length) { handleFileSelect(e.dataTransfer.files0); } }); fileInput.addEventListener(change, (e) > { if (e.target.files.length) handleFileSelect(e.target.files0); }); startUploadBtn.addEventListener(click, startUpload); downloadBtn.addEventListener(click, () > { window.location.href `${API_BASE}/download?instanceId${state.instanceId}`; }); preserveBtn.addEventListener(click, preserveUpload); copyUrlBtn.addEventListener(click, copyDownloadUrl); function handleFileSelect(file) { if (file.name.endsWith(.SC2Mod)) { showError(SC2Mod files are not supported yet); return; } if (file.name.endsWith(.SC2Map)) { showError(SC2Map files are not supported yet); return; } state.file file; fileName.textContent file.name; fileSize.textContent formatSize(file.size); fileInfo.classList.remove(hidden); startUploadBtn.disabled false; resetUI(); } function resetUI() { progressSection.classList.add(hidden); completeSection.classList.add(hidden); errorSection.classList.add(hidden); statusDetails.innerHTML ; shownMessages.clear(); } function formatSize(bytes) { if (bytes 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } function updateProgress(percent, label) { progressBar.style.width `${percent}%`; progressPercent.textContent `${Math.round(percent)}%`; if (label) progressLabel.textContent label; } function addStatusMessage(message, key) { // Use key to avoid duplicate messages; if no key provided, use message itself const messageKey key || message; if (shownMessages.has(messageKey)) { // Update existing message instead of adding duplicate const existingEl Array.from(statusDetails.children).find(el > el.dataset.key messageKey); if (existingEl) { existingEl.querySelector(span:last-child).textContent message; } return; } shownMessages.add(messageKey); const div document.createElement(div); div.className flex items-center gap-2; div.dataset.key messageKey; div.innerHTML `span classw-2 h-2 bg-terran rounded-full>/span>span>${message}/span>`; statusDetails.appendChild(div); } async function startUpload() { resetUI(); progressSection.classList.remove(hidden); state.status uploading; try { if (state.file.size DIRECT_UPLOAD_LIMIT) { await directUpload(); } else { await chunkedUpload(); } // Start polling for status pollStatus(); } catch (error) { showError(error.message); } } async function directUpload() { const formData new FormData(); formData.append(file, state.file); updateProgress(0, Uploading file...); const mergeWithExisting mergeWithExistingCheckbox.checked ? true : ; const queryParams mergeWithExisting ? `?mergeWithExisting${mergeWithExisting}` : ; const response await fetch(`${API_BASE}/upload${queryParams}`, { method: POST, body: formData }); if (!response.ok) { const error await response.json(); throw new Error(error.error || Upload failed); } const data await response.json(); state.instanceId data.instanceId; state.uploadId data.uploadId; updateProgress(100, Upload complete); addStatusMessage(Upload complete. Starting translation...); } async function chunkedUpload() { updateProgress(0, Initializing multipart upload...); // Get upload URL const initResponse await fetch(`${API_BASE}/upload-url?filename${encodeURIComponent(state.file.name)}`); const initData await initResponse.json(); const { sourceR2Key, outputR2Key, multipartUploadId, uploadId } initData; state.uploadId uploadId; // Split into chunks const chunks ; for (let start 0; start state.file.size; start + CHUNK_SIZE) { const end Math.min(start + CHUNK_SIZE, state.file.size); chunks.push({ start, end, data: state.file.slice(start, end) }); } updateProgress(0, `Uploading ${chunks.length} chunks...`); // Upload chunks concurrently (max 5 at a time) const MAX_CONCURRENCY 5; let completedCount 0; const uploadedParts ; const uploadChunk async (chunk, i) > { const formData new FormData(); formData.append(key, sourceR2Key); formData.append(uploadId, multipartUploadId); formData.append(partNumber, String(i + 1)); formData.append(part, chunk.data); const response await fetch(`${API_BASE}/upload-part`, { method: POST, body: formData }); if (!response.ok) throw new Error(`Failed to upload chunk ${i + 1}`); const part await response.json(); completedCount++; const progress (completedCount / chunks.length) * 50; updateProgress(progress, `Uploading chunks... (${completedCount}/${chunks.length})`); return { partNumber: part.partNumber, etag: part.etag }; }; for (let i 0; i chunks.length; i + MAX_CONCURRENCY) { const batch chunks.slice(i, i + MAX_CONCURRENCY); const results await Promise.all( batch.map((chunk, batchIndex) > uploadChunk(chunk, i + batchIndex)) ); uploadedParts.push(...results); } // Sort by partNumber (required by R2) uploadedParts.sort((a, b) > a.partNumber - b.partNumber); // Complete multipart upload updateProgress(50, Finalizing upload...); await fetch(`${API_BASE}/upload-complete`, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ key: sourceR2Key, uploadId: multipartUploadId, parts: uploadedParts }) }); // Start translation workflow updateProgress(55, Starting translation...); const translateResponse await fetch(`${API_BASE}/translate`, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ sourceR2Key, outputR2Key, mergeWithExisting: mergeWithExistingCheckbox.checked }) }); if (!translateResponse.ok) throw new Error(Failed to start translation); const translateData await translateResponse.json(); state.instanceId translateData.instanceId; updateProgress(60, Translation started); addStatusMessage(Upload complete. Starting translation...); } async function pollStatus() { state.status processing; const pollInterval setInterval(async () > { try { const response await fetch(`${API_BASE}/status?instanceId${state.instanceId}`); if (!response.ok) { clearInterval(pollInterval); showError(Failed to check status); return; } const data await response.json(); if (data.currentStep) { addStatusMessage(`Step: ${data.currentStep}`, step); } if (data.totalEntries && data.translatedEntries ! null) { const progress (data.translatedEntries / data.totalEntries) * 100; updateProgress(60 + (progress * 0.35), `Translating... (${data.translatedEntries}/${data.totalEntries})`); addStatusMessage(`Progress: ${data.translatedEntries}/${data.totalEntries} entries`, progress); } if (data.cacheHits ! null && data.cacheMisses ! null) { addStatusMessage(`Cache: ${data.cacheHits} hits, ${data.cacheMisses} new`, cache); } if (data.status complete) { clearInterval(pollInterval); updateProgress(100, Complete!); showComplete(); } else if (data.status errored) { clearInterval(pollInterval); showError(data.error || Translation failed); } } catch (error) { clearInterval(pollInterval); showError(error.message); } }, 5000); // Poll every 5 seconds } function showComplete() { state.status complete; progressSection.classList.add(hidden); completeSection.classList.remove(hidden); // Set initial download URL const downloadUrl window.location.origin + `${API_BASE}/download?instanceId${state.instanceId}`; downloadUrlEl.textContent downloadUrl; saveToHistory(state.file.name, state.instanceId, false); renderHistory(); } function showError(message) { state.status error; progressSection.classList.add(hidden); errorSection.classList.remove(hidden); errorMessage.textContent message; } async function preserveUpload() { try { preserveBtn.disabled true; preserveBtn.textContent Preserving...; const response await fetch(`${API_BASE}/preserve`, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ instanceId: state.instanceId }) }); if (!response.ok) throw new Error(Failed to preserve); const result await response.json(); state.preserved true; state.preservedOutputKey result.preservedOutput; // Update download URL to use preserved key const downloadUrl window.location.origin + `${API_BASE}/download?instanceId${state.instanceId}`; downloadUrlEl.textContent downloadUrl; preserveBtn.textContent Preserved!; preserveBtn.classList.remove(bg-protoss); preserveBtn.classList.add(bg-green-600); // Update history to mark as preserved saveToHistory(state.file.name, state.instanceId, true); renderHistory(); } catch (error) { preserveBtn.disabled false; preserveBtn.textContent Preserve Permanently; alert(Failed to preserve: + error.message); } } function copyDownloadUrl() { const url downloadUrlEl.textContent; navigator.clipboard.writeText(url).then(() > { const originalText copyUrlBtn.innerHTML; copyUrlBtn.innerHTML `svg classw-4 h-4 text-green-500 fillnone strokecurrentColor viewBox0 0 24 24> path stroke-linecapround stroke-linejoinround stroke-width2 dM5 13l4 4L19 7 /> /svg>`; setTimeout(() > { copyUrlBtn.innerHTML originalText; }, 2000); }); } function saveToHistory(filename, instanceId, preserved false) { const history JSON.parse(localStorage.getItem(sc2-translator-history) || ); // Remove any existing entry with the same instanceId const filtered history.filter(item > item.instanceId ! instanceId); // Add new entry at the beginning filtered.unshift({ filename, instanceId, preserved, date: new Date().toISOString() }); // Keep only 5 entries localStorage.setItem(sc2-translator-history, JSON.stringify(filtered.slice(0, 5))); } function renderHistory() { const history JSON.parse(localStorage.getItem(sc2-translator-history) || ); if (history.length 0) { recentList.innerHTML p classtext-gray-500 text-sm>No recent translations/p>; return; } recentList.innerHTML history.map(item > { const downloadUrl `${API_BASE}/download?instanceId${item.instanceId}`; return ` div classflex justify-between items-center p-3 bg-gray-800 rounded> div classflex-1 min-w-0> p classfont-medium truncate>${item.filename}/p> div classflex items-center gap-2> p classtext-xs text-gray-500>${new Date(item.date).toLocaleString()}/p> ${item.preserved ? span classtext-xs text-protoss>Preserved/span> : } /div> /div> a href${downloadUrl} classtext-terran hover:underline ml-2>Download/a> /div> `; }).join(); } // Initialize renderHistory(); /script>/body>/html>
Port 443
HTTP/1.1 200 OKDate: Tue, 10 Feb 2026 04:02:14 GMTContent-Type: text/html; charsetUTF-8Content-Length: 24735Connection: keep-aliveCf-Placement: local-PDXReport-To: {group:cf-nel,max_age:604800,endpoints:{url:https://a.nel.cloudflare.com/report/v4?shJAjTIXn8t4foSkKNdNBVF0TK%2FQeBruXFm7EAh7AXsZ%2FYv2cnUS%2BGFJuuMNWWvy8I7193dPzcSbN0PbqA8g%2BiGjMsoVTDlbcex%2BFFI5JWc07}}Nel: {report_to:cf-nel,success_fraction:0.0,max_age:604800}Server: cloudflareCF-RAY: 9cb8b5ba6e693741-PDXalt-svc: h3:443; ma86400 !DOCTYPE html>html langen>head> meta charsetUTF-8> meta nameviewport contentwidthdevice-width, initial-scale1.0> title>SC2 Campaign Translator/title> script srchttps://cdn.tailwindcss.com>/script> style> select { appearance: none; background-image: url(data:image/svg+xml;charsetUTF-8,%3csvg xmlnshttp://www.w3.org/2000/svg viewBox0 0 24 24 fillnone strokewhite stroke-width2 stroke-linecapround stroke-linejoinround%3e%3cpolyline points6 9 12 15 18 9%3e%3c/polyline%3e%3c/svg%3e); background-repeat: no-repeat; background-position: right 0.5rem center; background-size: 1.25em; padding-right: 2rem !important; } select:disabled { opacity: 0.8; -webkit-text-fill-color: white; /* Fix for Safari/Mac disabled text color */ color: white !important; } /style> script> tailwind.config { theme: { extend: { colors: { terran: #4A90E2, zerg: #E25C4A, protoss: #F5A623, } } } } /script>/head>body classbg-gray-900 text-white min-h-screen> div classcontainer mx-auto px-4 py-8 max-w-2xl> !-- Header --> header classtext-center mb-8> h1 classtext-3xl font-bold text-terran mb-2>Moukas Campaign Translator/h1> /header> !-- Upload Section --> section idupload-section classbg-gray-800 rounded-lg p-6> !-- Locale Selectors --> div classflex flex-col items-center mb-8 gap-2> div classflex items-center justify-center gap-6 p-4 bg-gray-900/50 rounded-xl border border-gray-700 w-full> div classflex items-center gap-3 titleCurrently only Chinese (Simplified) source is supported> label classtext-xs font-semibold text-gray-500 uppercase tracking-wider>From/label> select idsource-locale disabled classbg-gray-800 text-white text-sm rounded-lg px-3 py-2 border border-gray-600 cursor-not-allowed appearance-none min-w-180px titleCurrently only Chinese (Simplified) source is supported> option valuezhCN.SC2Data>π¨π³ Chinese (Simplified)/option> option valuekoKR.SC2Data>π°π· Korean/option> option valueenUS.SC2Data>πΊπΈ English/option> option valuedeDE.SC2Data>π©πͺ German/option> option valueesES.SC2Data>πͺπΈ Spanish/option> option valuefrFR.SC2Data>π«π· French/option> /select> /div> div classflex items-center> svg classw-5 h-5 text-gray-600 fillnone strokecurrentColor viewBox0 0 24 24> path stroke-linecapround stroke-linejoinround stroke-width2 dM13 7l5 5m0 0l-5 5m5-5H6 /> /svg> /div> div classflex items-center gap-3 titleCurrently only English target is supported> label classtext-xs font-semibold text-gray-500 uppercase tracking-wider>To/label> select idtarget-locale disabled classbg-gray-800 text-white text-sm rounded-lg px-3 py-2 border border-gray-600 cursor-not-allowed appearance-none min-w-180px titleCurrently only English target is supported> option valueenUS.SC2Data>πΊπΈ English/option> option valuekoKR.SC2Data>π°π· Korean/option> option valuezhCN.SC2Data>π¨π³ Chinese (Simplified)/option> option valuedeDE.SC2Data>π©πͺ German/option> option valueesES.SC2Data>πͺπΈ Spanish/option> option valuefrFR.SC2Data>π«π· French/option> /select> /div> /div> p classtext-10px text-gray-500 flex items-center gap-1> svg classw-3 h-3 fillnone strokecurrentColor viewBox0 0 24 24>path stroke-linecapround stroke-linejoinround stroke-width2 dM13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z />/svg> Currently only Chinese to English translation is supported /p> /div> div iddrop-zone classdrop-zone border-2 border-dashed border-gray-600 rounded-lg p-8 text-center cursor-pointer hover:border-terran transition> svg classw-16 h-16 mx-auto mb-4 text-gray-500 fillnone strokecurrentColor viewBox0 0 24 24> path stroke-linecapround stroke-linejoinround stroke-width2 dM7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12 /> /svg> p classtext-lg mb-2>Drag & drop your SC2 archive here/p> p classtext-sm text-gray-500>or click to browse (.zip, .SC2Mod, .SC2Map)/p> input typefile idfile-input classhidden accept.zip,.sc2mod,.sc2map,.SC2Mod,.SC2Map> /div> !-- File Info --> div idfile-info classhidden mt-4 p-4 bg-gray-700 rounded> div classflex justify-between items-center> div> p classfont-medium idfile-name>filename.zip/p> p classtext-sm text-gray-400 idfile-size>0 MB/p> /div> button idstart-upload classpx-4 py-2 bg-terran text-white rounded hover:bg-blue-600 transition>Start Translation/button> /div> !-- Options --> div classmt-3 pt-3 border-t border-gray-600> label classflex items-center gap-2 cursor-pointer group> input typecheckbox idmerge-with-existing classw-4 h-4 rounded bg-gray-800 border-gray-500 text-terran focus:ring-terran focus:ring-offset-gray-700> span classtext-sm text-gray-300 group-hover:text-white transition>Merge with existing target locale entries/span> span classrelative> svg classw-4 h-4 text-gray-500 hover:text-gray-300 cursor-help fillnone strokecurrentColor viewBox0 0 24 24> path stroke-linecapround stroke-linejoinround stroke-width2 dM13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z /> /svg> span classabsolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 text-xs text-white bg-gray-900 rounded shadow-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10> Enable this if you see missing values in-game like Param/Value/....br> This merges existing English strings with the translated ones. /span> /span> /label> /div> /div> !-- Progress Section --> div idprogress-section classhidden mt-4> div classflex justify-between mb-2> span idprogress-label classtext-sm>Uploading.../span> span idprogress-percent classtext-sm text-terran>0%/span> /div> div classw-full bg-gray-700 rounded-full h-2> div idprogress-bar classbg-terran h-2 rounded-full transition-all duration-300 stylewidth: 0%>/div> /div> !-- Status Messages --> div idstatus-details classmt-4 space-y-2 text-sm text-gray-400> !-- Dynamic status items --> /div> /div> !-- Complete Section --> div idcomplete-section classhidden mt-4 p-4 bg-green-900/50 rounded text-center> svg classw-16 h-16 mx-auto mb-4 text-green-500 fillnone strokecurrentColor viewBox0 0 24 24> path stroke-linecapround stroke-linejoinround stroke-width2 dM5 13l4 4L19 7 /> /svg> p classtext-xl mb-4 text-green-400>Translation Complete!/p> div classflex gap-4 justify-center mb-4> button iddownload-btn classpx-4 py-2 bg-terran text-white rounded hover:bg-blue-600 transition>Download Translated Campaign/button> button idpreserve-btn classpx-4 py-2 bg-protoss text-white rounded hover:bg-yellow-600 transition>Preserve Permanently/button> /div> !-- Download URL --> div classmt-4 p-3 bg-gray-800 rounded text-left> p classtext-xs text-gray-500 mb-1>Download URL:/p> div classflex items-center gap-2> code iddownload-url classflex-1 text-xs text-terran break-all bg-gray-900 px-2 py-1 rounded>/code> button idcopy-url-btn classtext-xs text-gray-400 hover:text-white transition titleCopy URL> svg classw-4 h-4 fillnone strokecurrentColor viewBox0 0 24 24> path stroke-linecapround stroke-linejoinround stroke-width2 dM8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z /> /svg> /button> /div> /div> /div> !-- Error Section --> div iderror-section classhidden mt-4 p-4 bg-red-900/50 rounded> p classfont-medium text-red-400>Error:/p> p iderror-message classtext-sm text-gray-300 mt-1>/p> p classtext-xs text-gray-500 mt-2>This is an alpha release. You may retry later./p> /div> /section> !-- Recent Translations --> section idrecent-section classmt-8> h2 classtext-xl font-bold mb-4>Recent Translations/h2> div idrecent-list classspace-y-2> p classtext-gray-500 text-sm>No recent translations/p> /div> /section> /div> script> // API base URL const API_BASE /api; const DIRECT_UPLOAD_LIMIT 100 * 1024 * 1024; // 100MB const CHUNK_SIZE 20 * 1024 * 1024; // 20MB // State let state { file: null, instanceId: null, uploadId: null, status: idle, preserved: false, preservedOutputKey: null }; // Track shown status messages to avoid duplicates const shownMessages new Set(); // DOM Elements const dropZone document.getElementById(drop-zone); const fileInput document.getElementById(file-input); const fileInfo document.getElementById(file-info); const fileName document.getElementById(file-name); const fileSize document.getElementById(file-size); const startUploadBtn document.getElementById(start-upload); const progressSection document.getElementById(progress-section); const progressBar document.getElementById(progress-bar); const progressPercent document.getElementById(progress-percent); const progressLabel document.getElementById(progress-label); const statusDetails document.getElementById(status-details); const completeSection document.getElementById(complete-section); const errorSection document.getElementById(error-section); const errorMessage document.getElementById(error-message); const downloadBtn document.getElementById(download-btn); const preserveBtn document.getElementById(preserve-btn); const recentList document.getElementById(recent-list); const downloadUrlEl document.getElementById(download-url); const copyUrlBtn document.getElementById(copy-url-btn); const mergeWithExistingCheckbox document.getElementById(merge-with-existing); // Event Listeners dropZone.addEventListener(click, () > fileInput.click()); dropZone.addEventListener(dragover, (e) > { e.preventDefault(); dropZone.classList.add(border-terran, bg-gray-700); }); dropZone.addEventListener(dragleave, () > { dropZone.classList.remove(border-terran, bg-gray-700); }); dropZone.addEventListener(drop, (e) > { e.preventDefault(); dropZone.classList.remove(border-terran, bg-gray-700); if (e.dataTransfer.files.length) { handleFileSelect(e.dataTransfer.files0); } }); fileInput.addEventListener(change, (e) > { if (e.target.files.length) handleFileSelect(e.target.files0); }); startUploadBtn.addEventListener(click, startUpload); downloadBtn.addEventListener(click, () > { window.location.href `${API_BASE}/download?instanceId${state.instanceId}`; }); preserveBtn.addEventListener(click, preserveUpload); copyUrlBtn.addEventListener(click, copyDownloadUrl); function handleFileSelect(file) { if (file.name.endsWith(.SC2Mod)) { showError(SC2Mod files are not supported yet); return; } if (file.name.endsWith(.SC2Map)) { showError(SC2Map files are not supported yet); return; } state.file file; fileName.textContent file.name; fileSize.textContent formatSize(file.size); fileInfo.classList.remove(hidden); startUploadBtn.disabled false; resetUI(); } function resetUI() { progressSection.classList.add(hidden); completeSection.classList.add(hidden); errorSection.classList.add(hidden); statusDetails.innerHTML ; shownMessages.clear(); } function formatSize(bytes) { if (bytes 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } function updateProgress(percent, label) { progressBar.style.width `${percent}%`; progressPercent.textContent `${Math.round(percent)}%`; if (label) progressLabel.textContent label; } function addStatusMessage(message, key) { // Use key to avoid duplicate messages; if no key provided, use message itself const messageKey key || message; if (shownMessages.has(messageKey)) { // Update existing message instead of adding duplicate const existingEl Array.from(statusDetails.children).find(el > el.dataset.key messageKey); if (existingEl) { existingEl.querySelector(span:last-child).textContent message; } return; } shownMessages.add(messageKey); const div document.createElement(div); div.className flex items-center gap-2; div.dataset.key messageKey; div.innerHTML `span classw-2 h-2 bg-terran rounded-full>/span>span>${message}/span>`; statusDetails.appendChild(div); } async function startUpload() { resetUI(); progressSection.classList.remove(hidden); state.status uploading; try { if (state.file.size DIRECT_UPLOAD_LIMIT) { await directUpload(); } else { await chunkedUpload(); } // Start polling for status pollStatus(); } catch (error) { showError(error.message); } } async function directUpload() { const formData new FormData(); formData.append(file, state.file); updateProgress(0, Uploading file...); const mergeWithExisting mergeWithExistingCheckbox.checked ? true : ; const queryParams mergeWithExisting ? `?mergeWithExisting${mergeWithExisting}` : ; const response await fetch(`${API_BASE}/upload${queryParams}`, { method: POST, body: formData }); if (!response.ok) { const error await response.json(); throw new Error(error.error || Upload failed); } const data await response.json(); state.instanceId data.instanceId; state.uploadId data.uploadId; updateProgress(100, Upload complete); addStatusMessage(Upload complete. Starting translation...); } async function chunkedUpload() { updateProgress(0, Initializing multipart upload...); // Get upload URL const initResponse await fetch(`${API_BASE}/upload-url?filename${encodeURIComponent(state.file.name)}`); const initData await initResponse.json(); const { sourceR2Key, outputR2Key, multipartUploadId, uploadId } initData; state.uploadId uploadId; // Split into chunks const chunks ; for (let start 0; start state.file.size; start + CHUNK_SIZE) { const end Math.min(start + CHUNK_SIZE, state.file.size); chunks.push({ start, end, data: state.file.slice(start, end) }); } updateProgress(0, `Uploading ${chunks.length} chunks...`); // Upload chunks concurrently (max 5 at a time) const MAX_CONCURRENCY 5; let completedCount 0; const uploadedParts ; const uploadChunk async (chunk, i) > { const formData new FormData(); formData.append(key, sourceR2Key); formData.append(uploadId, multipartUploadId); formData.append(partNumber, String(i + 1)); formData.append(part, chunk.data); const response await fetch(`${API_BASE}/upload-part`, { method: POST, body: formData }); if (!response.ok) throw new Error(`Failed to upload chunk ${i + 1}`); const part await response.json(); completedCount++; const progress (completedCount / chunks.length) * 50; updateProgress(progress, `Uploading chunks... (${completedCount}/${chunks.length})`); return { partNumber: part.partNumber, etag: part.etag }; }; for (let i 0; i chunks.length; i + MAX_CONCURRENCY) { const batch chunks.slice(i, i + MAX_CONCURRENCY); const results await Promise.all( batch.map((chunk, batchIndex) > uploadChunk(chunk, i + batchIndex)) ); uploadedParts.push(...results); } // Sort by partNumber (required by R2) uploadedParts.sort((a, b) > a.partNumber - b.partNumber); // Complete multipart upload updateProgress(50, Finalizing upload...); await fetch(`${API_BASE}/upload-complete`, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ key: sourceR2Key, uploadId: multipartUploadId, parts: uploadedParts }) }); // Start translation workflow updateProgress(55, Starting translation...); const translateResponse await fetch(`${API_BASE}/translate`, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ sourceR2Key, outputR2Key, mergeWithExisting: mergeWithExistingCheckbox.checked }) }); if (!translateResponse.ok) throw new Error(Failed to start translation); const translateData await translateResponse.json(); state.instanceId translateData.instanceId; updateProgress(60, Translation started); addStatusMessage(Upload complete. Starting translation...); } async function pollStatus() { state.status processing; const pollInterval setInterval(async () > { try { const response await fetch(`${API_BASE}/status?instanceId${state.instanceId}`); if (!response.ok) { clearInterval(pollInterval); showError(Failed to check status); return; } const data await response.json(); if (data.currentStep) { addStatusMessage(`Step: ${data.currentStep}`, step); } if (data.totalEntries && data.translatedEntries ! null) { const progress (data.translatedEntries / data.totalEntries) * 100; updateProgress(60 + (progress * 0.35), `Translating... (${data.translatedEntries}/${data.totalEntries})`); addStatusMessage(`Progress: ${data.translatedEntries}/${data.totalEntries} entries`, progress); } if (data.cacheHits ! null && data.cacheMisses ! null) { addStatusMessage(`Cache: ${data.cacheHits} hits, ${data.cacheMisses} new`, cache); } if (data.status complete) { clearInterval(pollInterval); updateProgress(100, Complete!); showComplete(); } else if (data.status errored) { clearInterval(pollInterval); showError(data.error || Translation failed); } } catch (error) { clearInterval(pollInterval); showError(error.message); } }, 5000); // Poll every 5 seconds } function showComplete() { state.status complete; progressSection.classList.add(hidden); completeSection.classList.remove(hidden); // Set initial download URL const downloadUrl window.location.origin + `${API_BASE}/download?instanceId${state.instanceId}`; downloadUrlEl.textContent downloadUrl; saveToHistory(state.file.name, state.instanceId, false); renderHistory(); } function showError(message) { state.status error; progressSection.classList.add(hidden); errorSection.classList.remove(hidden); errorMessage.textContent message; } async function preserveUpload() { try { preserveBtn.disabled true; preserveBtn.textContent Preserving...; const response await fetch(`${API_BASE}/preserve`, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ instanceId: state.instanceId }) }); if (!response.ok) throw new Error(Failed to preserve); const result await response.json(); state.preserved true; state.preservedOutputKey result.preservedOutput; // Update download URL to use preserved key const downloadUrl window.location.origin + `${API_BASE}/download?instanceId${state.instanceId}`; downloadUrlEl.textContent downloadUrl; preserveBtn.textContent Preserved!; preserveBtn.classList.remove(bg-protoss); preserveBtn.classList.add(bg-green-600); // Update history to mark as preserved saveToHistory(state.file.name, state.instanceId, true); renderHistory(); } catch (error) { preserveBtn.disabled false; preserveBtn.textContent Preserve Permanently; alert(Failed to preserve: + error.message); } } function copyDownloadUrl() { const url downloadUrlEl.textContent; navigator.clipboard.writeText(url).then(() > { const originalText copyUrlBtn.innerHTML; copyUrlBtn.innerHTML `svg classw-4 h-4 text-green-500 fillnone strokecurrentColor viewBox0 0 24 24> path stroke-linecapround stroke-linejoinround stroke-width2 dM5 13l4 4L19 7 /> /svg>`; setTimeout(() > { copyUrlBtn.innerHTML originalText; }, 2000); }); } function saveToHistory(filename, instanceId, preserved false) { const history JSON.parse(localStorage.getItem(sc2-translator-history) || ); // Remove any existing entry with the same instanceId const filtered history.filter(item > item.instanceId ! instanceId); // Add new entry at the beginning filtered.unshift({ filename, instanceId, preserved, date: new Date().toISOString() }); // Keep only 5 entries localStorage.setItem(sc2-translator-history, JSON.stringify(filtered.slice(0, 5))); } function renderHistory() { const history JSON.parse(localStorage.getItem(sc2-translator-history) || ); if (history.length 0) { recentList.innerHTML p classtext-gray-500 text-sm>No recent translations/p>; return; } recentList.innerHTML history.map(item > { const downloadUrl `${API_BASE}/download?instanceId${item.instanceId}`; return ` div classflex justify-between items-center p-3 bg-gray-800 rounded> div classflex-1 min-w-0> p classfont-medium truncate>${item.filename}/p> div classflex items-center gap-2> p classtext-xs text-gray-500>${new Date(item.date).toLocaleString()}/p> ${item.preserved ? span classtext-xs text-protoss>Preserved/span> : } /div> /div> a href${downloadUrl} classtext-terran hover:underline ml-2>Download/a> /div> `; }).join(); } // Initialize renderHistory(); /script>/body>/html>
View on OTX
|
View on ThreatMiner
Please enable JavaScript to view the
comments powered by Disqus.
Data with thanks to
AlienVault OTX
,
VirusTotal
,
Malwr
and
others
. [
Sitemap
]