const $ = (id) => document.getElementById(id);
const logEl = $("log");
let selectedFile = null;
let projectId = null;
let pollTimer = null;
function log(msg) {
logEl.textContent += `[${new Date().toLocaleTimeString()}] ${msg}\n`;
logEl.scrollTop = logEl.scrollHeight;
}
function apiBase() {
return $("apiBase").value.replace(/\/+$/, "");
}
function headers(extra) {
const h = Object.assign({}, extra || {});
const key = $("apiKey").value.trim();
if (key) h["X-API-Key"] = key;
return h;
}
async function api(path, options) {
const res = await fetch(apiBase() + path, options || {});
const text = await res.text();
let data = null;
try { data = text ? JSON.parse(text) : null; } catch (e) { /* not json */ }
if (!res.ok) throw new Error((data && data.detail) || text || res.status);
return data;
}
// ---------------------------------------------------------------- languages
const FALLBACK_LANGS = { tr: "Turkish", en: "English", ja: "Japanese", de: "German", es: "Spanish" };
function fillLanguages(map) {
const sel = $("targetLang");
sel.innerHTML = "";
for (const [code, name] of Object.entries(map)) {
sel.add(new Option(`${name} (${code})`, code));
}
sel.value = "tr";
}
async function loadLanguages() {
try {
fillLanguages(await api("/languages", { headers: headers() }));
} catch (e) {
fillLanguages(FALLBACK_LANGS);
}
}
// ------------------------------------------------------------------- upload
function setFile(file) {
if (!file) return;
if (!file.name.toLowerCase().endsWith(".zip")) {
log("Yalnızca .zip dosyası yükleyebilirsiniz.");
return;
}
selectedFile = file;
$("uploadInfo").textContent = `${file.name} — ${(file.size / 1048576).toFixed(1)} MB`;
$("btnUpload").disabled = false;
}
$("dropZone").addEventListener("click", () => $("fileInput").click());
$("fileInput").addEventListener("change", (e) => setFile(e.target.files[0]));
["dragenter", "dragover"].forEach((ev) =>
$("dropZone").addEventListener(ev, (e) => {
e.preventDefault();
$("dropZone").classList.add("dragging");
})
);
["dragleave", "drop"].forEach((ev) =>
$("dropZone").addEventListener(ev, (e) => {
e.preventDefault();
$("dropZone").classList.remove("dragging");
})
);
$("dropZone").addEventListener("drop", (e) => setFile(e.dataTransfer.files[0]));
$("btnUpload").addEventListener("click", async () => {
if (!selectedFile) return;
const body = new FormData();
body.append("file", selectedFile);
$("btnUpload").disabled = true;
log(`Yükleniyor: ${selectedFile.name} ...`);
try {
const st = await api("/renpy/upload", { method: "POST", headers: headers(), body });
projectId = st.id;
log(`Analiz tamam — ${st.script_files} script dosyası, ${st.total_units} benzersiz metin, ${st.total_slots} konum.`);
if (st.compiled_slots) {
log(`${st.compiled_slots} metin derlenmiş .rpyc içinden okundu; bunlar için ayrı bir çeviri dosyası üretilecek.`);
}
if (st.unreadable_files && st.unreadable_files.length) {
log(`Okunamayan dosyalar atlandı: ${st.unreadable_files.join(", ")}`);
}
render(st);
} catch (e) {
log("Yükleme hatası: " + e.message);
} finally {
$("btnUpload").disabled = false;
}
});
// -------------------------------------------------------------------- render
function stat(num, label) {
return `
`;
}
const STATUS_TR = {
ready: "hazır", translating: "çevriliyor", done: "tamamlandı",
cancelled: "durduruldu", failed: "hata",
};
function render(st) {
$("projectPanel").hidden = false;
$("projectStats").innerHTML = [
stat(st.script_files || 0, "Script Dosyası"),
stat(st.total_units, "Benzersiz Metin"),
stat(st.total_slots, "Toplam Konum"),
stat(STATUS_TR[st.status] || st.status, "Durum"),
stat(st.failed_units || 0, "Hata"),
].join("");
$("overallBar").style.width = st.percent + "%";
$("overallPct").textContent = st.percent + "%";
$("overallNote").textContent =
`${st.translated_units} / ${st.total_units} metin çevrildi` +
(st.target_lang ? ` — hedef: ${st.target_lang}` : "");
const note = $("compiledNote");
if (st.compiled_slots) {
note.hidden = false;
note.textContent =
`${st.compiled_slots} metin derlenmiş .rpyc dosyalarından geliyor (diyalog, menü ` +
`seçenekleri ve arayüz metinleri — buton, sekme, mağaza/istatistik ekranları dahil). ` +
`Bu dosyalar düzenlenemediği için çeviriler, indirilen zip'teki tl/ klasörü ve ` +
`hymt_translate.rpy üzerinden oyuna uygulanır.`;
} else {
note.hidden = true;
}
const staleNote = $("staleRpycNote");
if (st.stale_rpyc && st.stale_rpyc.length) {
staleNote.hidden = false;
staleNote.innerHTML =
`⚠ Projenizde ${st.stale_rpyc.length} dosyanın hem .rpy hem eski bir .rpyc kopyası var. ` +
`İndirilen zip'in içindeki HYMT_DELETE_THESE_RPYC_FIRST.txt dosyasını açın ve ` +
`oradaki .rpyc dosyalarını çevrilen .rpy'leri kopyalamadan ÖNCE oyun klasörünüzden silin — ` +
`yoksa Ren'Py eski derlenmiş sürümü çalıştırıp çeviriyle çakışabilir ve oyun içi hataya yol açabilir.`;
} else {
staleNote.hidden = true;
}
$("fileList").innerHTML = st.files.map((f) => `
${f.file}
${f.done}/${f.total}
`).join("");
const running = st.running || st.status === "translating";
$("btnStart").hidden = running;
$("btnStart").textContent = st.translated_units > 0 && st.status !== "done"
? "Kaldığı Yerden Devam Et" : "Çeviriyi Başlat";
$("btnCancel").hidden = !running;
if (running) startPolling(); else stopPolling();
}
// ------------------------------------------------------------------ polling
function startPolling() {
if (pollTimer) return;
pollTimer = setInterval(refresh, 2000);
}
function stopPolling() {
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
}
async function refresh() {
if (!projectId) return;
try {
const st = await api(`/renpy/${projectId}/status`, { headers: headers() });
const wasRunning = !$("btnCancel").hidden;
render(st);
if (wasRunning && !(st.running || st.status === "translating")) {
log(`Çeviri ${STATUS_TR[st.status] || st.status}. ${st.translated_units}/${st.total_units} metin.` +
(st.failed_units ? ` ${st.failed_units} satırda model hatası oldu, kaynak korundu.` : ""));
}
} catch (e) {
log("Durum alınamadı: " + e.message);
stopPolling();
}
}
// ------------------------------------------------------------------ actions
$("btnStart").addEventListener("click", async () => {
if (!projectId) return;
try {
log("Çeviri başlatılıyor...");
await api(`/renpy/${projectId}/start`, {
method: "POST",
headers: headers({ "Content-Type": "application/json" }),
body: JSON.stringify({ target_lang: $("targetLang").value }),
});
startPolling();
refresh();
} catch (e) {
log("Başlatılamadı: " + e.message);
}
});
$("btnCancel").addEventListener("click", async () => {
try {
await api(`/renpy/${projectId}/cancel`, { method: "POST", headers: headers() });
log("Durdurma istendi — devam eden istekler bitince duracak.");
} catch (e) {
log("Durdurulamadı: " + e.message);
}
});
$("btnDownload").addEventListener("click", async () => {
if (!projectId) return;
log("Paketleniyor...");
try {
// Fetched rather than linked so the API key header can be sent, and so a
// failure surfaces as a log line instead of a broken download.
const res = await fetch(`${apiBase()}/renpy/${projectId}/download`, { headers: headers() });
if (!res.ok) throw new Error(await res.text());
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "renpy-translated.zip";
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
log(`İndirildi (${(blob.size / 1048576).toFixed(2)} MB). Zip'i oyunun kök klasörüne açın.`);
} catch (e) {
log("İndirilemedi: " + e.message);
}
});
$("btnDelete").addEventListener("click", async () => {
if (!projectId) return;
try {
await api(`/renpy/${projectId}`, { method: "DELETE", headers: headers() });
log("Proje silindi.");
stopPolling();
projectId = null;
$("projectPanel").hidden = true;
} catch (e) {
log("Silinemedi: " + e.message);
}
});
// --------------------------------------------------------------- initialise
if (location.protocol === "http:" || location.protocol === "https:") {
$("apiBase").value = location.origin;
}
loadLanguages();
// Pick up a job that is already running - e.g. after a page reload, or after
// the Space restarted and resumed the run on its own.
(async () => {
try {
const list = await api("/renpy", { headers: headers() });
if (list.projects && list.projects.length) {
const active = list.projects.find((p) => p.running) || list.projects[0];
projectId = active.id;
render(active);
log(`Mevcut proje bulundu: ${active.name || active.id} (${STATUS_TR[active.status] || active.status}).`);
} else {
log("Arayüz hazır. Bir .rpy/.rpyc zip'i veya game klasörü yükleyin.");
}
} catch (e) {
log("Arayüz hazır. API'ye bağlanılamadı: " + e.message);
}
})();