Docs

#### Translations

Only missing
Return to domains Saving ... Save
Vvveb 0.2 is now available! Update now
You successfully read this important alert message.
You successfully read this important warning message.
You successfully read this important alert message.

Text | Translation
--- | ---
Text | Translation

Text </text> </div> <div class=""> <label for="translation-textarea" class="small py-1">Translation</label> <textarea id="translation-textarea" class="form-control" rows="4"></text> </div> </div> </div> </div> </div> <input type="hidden" name="csrf" id="csrf" data-v-csrf> <div id="main-footer"> Powered by <a href="https://www.vvveb.com" target="_blank">Vvveb</a> </div> </div> </div> <style> table[data-v-translations] thead th { resize:horizontal; overflow:auto } table[data-v-translations] tbody { cursor:pointer; } table[data-v-translations] tbody tr.active td { background-color: var(--bs-primary) !important; color: var(--bs-light); } table[data-v-translations] tbody tr.changed td { background-color: var(--bs-success-bg-subtle); } table[data-v-translations] tbody tr td:empty { background-color: rgba(var(--bs-warning-rgb), 0.1); } </style> <div class="modal fade" id="searchModal" tabindex="-1" aria-labelledby="searchModalLabel" aria-hidden="true"> <div class="modal-dialog modal-dialog-scrollable"> <button type="button" class="btn-close btn-close-white small" data-bs-dismiss="modal" aria-label="Close"></button> <div class="modal-content"> <div class="modal-body p-0"> <form action="{$this.adminPath}/index.php?module=tools/search" method="get"> <input type="hidden" name="module" value="tools/search"> <div class="d-flex"> <div class="input-group"> <input type="search" name="search" class="form-control border-0 p-3" id="headerSearch" placeholder="Start typing to search" data-v-vvveb-action="search.autocomplete" data-selector=".search-results" data-v-vvveb-on="keyup"> <button class="btn btn-outline-primary border-0" type="submit" title="Search"> <div class="la-flip-horizontal"> <i class="la la-search la-lg" aria-hidden="true"></i> </div> </button> </div> </div> <div class="loading d-none d-flex justify-content-center m-5"> <div class="spinner-border" role="status"> <span class="visually-hidden">Loading...</span> </div> </div> </form> <div class="search-results"> </div> </div> </div> </div> </div> <div id="page-loading-status" class="progress progress-bar"></div> <!-- <script id="jquery-js" src="js/jquery.min.js"></script> --> <script id="popper-js" src="js/popper.min.js"></script> <script id="bootstrap-js" src="js/bootstrap.min.js"></script> <script id="theme-js" src="js/theme.js"></script> <!-- vvveb modules code --> <script id="admin-js" type="module" src="/js/admin.js"></script> <script> var rbracket = /\[\]$/; function buildParams( prefix, obj, add ) { var name; if ( Array.isArray( obj ) ) { // Serialize array item. for(var key in obj) { let v = obj[key]; if ( rbracket.test( prefix ) ) { // Treat each array item as a scalar. add( prefix, v ); } else { // Item is non-scalar (array or object), encode its numeric index. buildParams( prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]", v, add ); } } } else if ( typeof obj === "object" ) { // Serialize object item. for ( name in obj ) { buildParams( prefix + "[" + name + "]", obj[ name ], add ); } } else { // Serialize scalar item. add( prefix, obj ); } } // Serialize an array of form elements or a set of // key/values into a query string function nestedFormData( a ) { var prefix, s = [], add = function( key, valueOrFunction ) { // If value is a function, invoke it and use its return value var value = typeof valueOrFunction === "function" ? valueOrFunction() : valueOrFunction; s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value == null ? "" : value ); }; if ( a == null ) { return ""; } // If an array was passed in, assume that it is an array of form elements. if ( Array.isArray( a ) || ( Object.is( a ) ) ) { // Serialize the form elements for(var key in object) { let v = object[key]; //jQuery.each( a, function() { add( key, v ); }; } else { // If traditional, encode the "old" way (the way 1.3.2 or older // did it), otherwise encode params recursively. for ( prefix in a ) { buildParams( prefix, a[ prefix ], add ); } } // Return the resulting serialization return s.join( "&" ); }; let selectedTranslation; let selectedRow; document.querySelector("table[data-v-translations] tbody").addEventListener("click", function (e) { let element = e.target.closest("tr"); let columns; if (element) { columns = element.querySelectorAll("td"); document.getElementById("text-textarea").value = columns[0].innerHTML; document.getElementById("translation-textarea").value = columns[1].innerHTML; selectedTranslation = columns[1]; selectedRow = element; document.querySelectorAll("table[data-v-translations] tbody tr").forEach(e => e.classList.remove("active")); this.classList.add("active"); } }); let search = function (e) { let searchText = this.value; delay(() => { if (searchText.length) { document.querySelectorAll("table[data-v-translations] tbody tr").forEach(e => { e.style.display = "none"; if (e.innerHTML.indexOf(searchText) > -1) e.style.display = ""; }); } else { document.querySelectorAll("table[data-v-translations] tbody tr").forEach(e => e.style.display = ""); } }, 300); }; let filter = document.getElementById("filter"); filter.addEventListener("keyup", search); filter.addEventListener("search", search); let save = function (e) { let btn = this; let translations = {}; let saveUrl = this.href; let changedRows = document.querySelectorAll("table[data-v-translations] tbody tr.changed"); changedRows.forEach(function (e) { let columns = e.querySelectorAll("td"); translations[columns[0].innerHTML] = columns[1].innerHTML; }); btn.querySelector(".loading")?.classList.toggle("d-none"); btn.querySelector(".button-text")?.classList.toggle("d-none"); fetch(saveUrl, {method:"POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: nestedFormData({translations,csrf:document.getElementById("csrf")?.value})}) .then(response => { if (!response.ok) { return Promise.reject(response); } return response.json() }) .then(data => { btn.querySelector(".loading")?.classList.toggle("d-none"); btn.querySelector(".button-text")?.classList.toggle("d-none"); displayToast("bg-success", "Save", data.message ?? data); changedRows.forEach(e => e.classList.remove("changed")); }) .catch(error => { console.log(error.statusText); displayToast("bg-danger", "Error", "Error saving!\n" + error.responseText); //displayToast("bg-danger", "Revision", "Error!"); }); e.preventDefault(); return false; }; document.getElementById("save-btn").addEventListener("click", save); document.getElementById("add-text").addEventListener("click", function () { let text = prompt("Enter new text"); if (text) { document.querySelector("[data-v-translations] tbody").append(generateElements(`<tr class="changed"><td>${text}</td><td></td></tr>`)[0]); window.scrollTo(0, document.body.scrollHeight); } }); document.getElementById("translation-textarea").addEventListener("keyup", function (e) { if (selectedTranslation) { delay(() => { selectedTranslation.innerHTML = this.value; selectedRow.classList.add("changed"); }, 200); } }); /* ---------- AI bulk translate (requires AI Assistant plugin) ---------- */ const aiLocaleNames = {en:"English",ro:"Romanian",fr:"French",de:"German",es:"Spanish",it:"Italian",pt:"Portuguese",nl:"Dutch",ru:"Russian",uk:"Ukrainian",pl:"Polish",hu:"Hungarian",bg:"Bulgarian",cs:"Czech",sk:"Slovak",sl:"Slovenian",hr:"Croatian",sr:"Serbian",el:"Greek",tr:"Turkish",ar:"Arabic",he:"Hebrew",fa:"Persian",hi:"Hindi",zh:"Chinese",ja:"Japanese",ko:"Korean",vi:"Vietnamese",th:"Thai",id:"Indonesian",ms:"Malay",sv:"Swedish",da:"Danish",fi:"Finnish",no:"Norwegian",nb:"Norwegian",nn:"Norwegian",ca:"Catalan",gl:"Galician",eu:"Basque",lt:"Lithuanian",lv:"Latvian",et:"Estonian"}; function aiTargetLanguage() { const lang = (new URLSearchParams(location.search).get("lang") || "").trim(); const prefix = lang.split(/[_-]/)[0].toLowerCase(); return aiLocaleNames[prefix] || (lang || "the target language"); } function aiParseJsonArray(text) { let s = String(text || "").trim(); s = s.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, ""); const start = s.indexOf("["); const end = s.lastIndexOf("]"); if (start !== -1 && end !== -1 && end > start) s = s.slice(start, end + 1); const parsed = JSON.parse(s); if (!Array.isArray(parsed)) throw new Error("not an array"); return parsed.map(v => String(v ?? "")); } async function aiTranslateBatch(texts, targetLang, options) { const payload = VvvebAi.buildPayload({ text: "Translate the following texts from English to " + targetLang + ". Preserve HTML tags and placeholders like %s, {name} or {{var}} exactly as they are. Return ONLY a JSON array of translated strings in the same order, no preamble, no code fences:\n\n" + JSON.stringify(texts), system: "You are a professional translator. You always reply with a raw JSON array of strings and nothing else.", provider: options.provider, model: options.model, temperature: 0.2, maxTokens: options.maxTokens, contextSize: options.contextSize, stream: false, options: { key: options.key, url: options.url, model: options.model, temperature: 0.2, max_tokens: options.maxTokens, contextSize: options.contextSize, apiMode: options.apiMode, proxyUrl: options.proxyUrl, proxyCsrf: options.csrf } }); const result = await VvvebAi.sendQuery(payload, () => {}); return aiParseJsonArray(result.text); } document.getElementById("translate-btn").addEventListener("click", async function () { const btn = this; const label = btn.querySelector("span"); const origLabel = label ? label.textContent : ""; if (typeof window.VvvebAi === "undefined" || typeof window.VvvebAi.sendQuery !== "function") { displayToast("bg-danger", "Error", "AI assistant is unavailable. Enable the AI Assistant plugin."); return; } const options = window.VvvebAi.options ? window.VvvebAi.options() : {}; if (options.apiMode !== "proxy" && !options.key) { displayToast("bg-danger", "Error", "No AI API key configured! Enter a valid key in Plugins \u2192 AI Assistant \u2192 Settings."); return; } const missingOnly = document.getElementById("translate-missing-only")?.checked; const rows = Array.from(document.querySelectorAll("table[data-v-translations] tbody tr")).filter(tr => { const cols = tr.querySelectorAll("td"); if (cols.length < 2) return false; if (!cols[0].textContent.trim()) return false; if (missingOnly && cols[1].textContent.trim() !== "") return false; return true; }); if (!rows.length) { displayToast("bg-info", "Info", missingOnly ? "No missing translations \u2013 everything is already translated." : "No rows to translate."); return; } const targetLang = aiTargetLanguage(); btn.disabled = true; const BATCH = 25; let done = 0, failed = 0; try { for (let i = 0; i < rows.length; i += BATCH) { const batch = rows.slice(i, i + BATCH); if (label) label.textContent = `Translating ${Math.min(i + BATCH, rows.length)}/${rows.length}\u2026`; try { const texts = batch.map(tr => tr.querySelectorAll("td")[0].innerHTML.trim()); const translated = await aiTranslateBatch(texts, targetLang, options); batch.forEach((tr, idx) => { if (idx >= translated.length || !translated[idx].trim()) { failed++; return; } const cols = tr.querySelectorAll("td"); cols[1].innerHTML = translated[idx]; tr.classList.add("changed"); done++; }); } catch (e) { console.error("AI translate batch failed", e); failed += batch.length; } } if (done) displayToast("bg-success", "Translate", `Translated ${done} row${done === 1 ? "" : "s"}${failed ? ` (${failed} failed)` : ""}. Press Save to keep them.`); else displayToast("bg-danger", "Error", "Translation failed, nothing was updated."); } finally { btn.disabled = false; if (label) label.textContent = origLabel; } }); </script> <!-- Modal --> <div class="modal fade" id="heartBeatLogin" data-bs-backdrop="static" tabindex="-1" aria-labelledby="heartBeatLoginLabel" aria-hidden="true"> <div class="modal-dialog modal-dialog-centered"> <div class="modal-content"> <div class="modal-header"> <span class="modal-title" id="heartBeatLoginLabel"> <h5>Session has expired</h5> <span>Login to resume your work</span> </span> <button type="button" class="btn-close me-2" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body p-5"> </div> </div> </div> </div> <!-- save toast --> <div class="toast-container position-fixed end-0 bottom-0 me-4 my-4"> <div class="toast " role="alert" aria-live="assertive" aria-atomic="true" id="bottom-toast"> <div class="toast-header bg-success bg-opacity-10 border-0 py-1"> <strong class="me-auto">Page save</strong> <!-- <small class="badge bg-success">status</small> --> <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button> </div> <div class="toast-body py-2"> <div class="flex-grow-1"> <div class="message"> <div>Elements saved!</div> <div>Template backup was saved!</div> <div>Template was saved!</div> </div> <div> <!-- <a class="btn btn-success btn-icon btn-sm w-100 mt-2" href="">View page</a></div> --> </div> </div> </div> </div> <div class="toast-container position-fixed end-0 top-0 me-4 my-4"> <div class="toast " role="alert" aria-live="assertive" aria-atomic="true" id="top-toast"> <div class="toast-header bg-success bg-opacity-10 border-0 py-1"> <strong class="me-auto">Page save</strong> <!-- <small class="badge bg-success">status</small> --> <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button> </div> <div class="toast-body py-2"> <div class="flex-grow-1"> <div class="message"> <div>Elements saved!</div> <div>Template backup was saved!</div> <div>Template was saved!</div> </div> <div> <!-- <a class="btn btn-success btn-icon btn-sm w-100 mt-2" href="">View page</a></div> --> </div> </div> </div> </div> </body> </html>