// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0 // API host/endpoint var BaseUrl = window.location.protocol + "//" + window.location.host + "{{ url_prefix }}" ; var htmlRegex = /<(.*)>.*?|<(.*)\/>/; document.addEventListener('DOMContentLoaded', function(){ var sidenavElems = document.querySelectorAll('.sidenav'); var sidenavInstances = M.Sidenav.init(sidenavElems); var app = new Vue({ el: '#app', delimiters: ['[[',']]'], data: { BaseUrl: BaseUrl, loading: true, error: "", langs: [], settings: {}, sourceLang: "", targetLang: "", loadingTranslation: false, inputText: "", inputTextareaHeight: 250, savedTanslatedText: "", translatedText: "", output: "", charactersLimit: -1, detectedLangText: "", copyTextLabel: {{ _e("Copy text") }}, suggestions: false, isSuggesting: false, supportedFilesFormat : [], translationType: "text", inputFile: false, loadingFileTranslation: false, translatedFileUrl: false, filesTranslation: true, frontendTimeout: 500, apiSecret: "{{ api_secret }}" }, mounted: function() { const self = this; self.$el.classList.add("loaded"); const settingsRequest = new XMLHttpRequest(); settingsRequest.open("GET", BaseUrl + "/frontend/settings", true); const langsRequest = new XMLHttpRequest(); langsRequest.open("GET", BaseUrl + "/languages", true); settingsRequest.onload = function() { if (this.status >= 200 && this.status < 400) { self.settings = JSON.parse(this.response); self.sourceLang = self.settings.language.source.code; self.targetLang = self.settings.language.target.code; self.charactersLimit = self.settings.charLimit; self.suggestions = self.settings.suggestions; self.supportedFilesFormat = self.settings.supportedFilesFormat; self.filesTranslation = self.settings.filesTranslation; self.frontendTimeout = self.settings.frontendTimeout; if (langsRequest.response) { handleLangsResponse(self, langsRequest); } else { langsRequest.onload = function() { handleLangsResponse(self, this); var hostname = window.location.hostname.toLowerCase(); if (hostname.indexOf("libretranslate.") === 0 && !hostname.endsWith(".com")){ self.error = "This website might be in violation of our trademark guidelines: https://github.com/LibreTranslate/LibreTranslate/blob/main/TRADEMARK.md"; } } } } else { self.error = {{ _e("Cannot load %(url)s", url="/frontend/settings") }}; self.loading = false; } }; settingsRequest.onerror = function() { self.error = {{ _e("Cannot load %(url)s", url="/frontend/settings") }}; self.loading = false; }; langsRequest.onerror = function() { self.error = {{ _e("Cannot load %(url)s", url="/languages") }}; self.loading = false; }; settingsRequest.send(); langsRequest.send(); }, updated: function(){ if (this.isSuggesting) return; M.FormSelect.init(this.$refs.sourceLangDropdown); M.FormSelect.init(this.$refs.targetLangDropdown); if (this.$refs.inputTextarea){ this.$refs.inputTextarea.focus() if (this.inputText === ""){ this.$refs.inputTextarea.style.height = this.inputTextareaHeight + "px"; this.$refs.translatedTextarea.style.height = this.inputTextareaHeight + "px"; } else{ this.$refs.inputTextarea.style.height = this.$refs.translatedTextarea.style.height = "1px"; this.$refs.inputTextarea.style.height = Math.max(this.inputTextareaHeight, this.$refs.inputTextarea.scrollHeight + 32) + "px"; this.$refs.translatedTextarea.style.height = Math.max(this.inputTextareaHeight, this.$refs.translatedTextarea.scrollHeight + 32) + "px"; } } // Update "selected" attribute (to overcome a vue.js limitation) // but properly display checkmarks on supported browsers. // Also change the <select> width value depending on the <option> length if (this.$refs.sourceLangDropdown) { updateSelectedAttribute(this.$refs.sourceLangDropdown, this.sourceLang); } if (this.$refs.targetLangDropdown) { updateSelectedAttribute(this.$refs.targetLangDropdown, this.targetLang); } }, computed: { requestCode: function(){ return ['const res = await fetch("' + this.BaseUrl + '/translate", {', ' method: "POST",', ' body: JSON.stringify({', ' q: ' + this.$options.filters.escape(this.inputText) + ',', ' source: ' + this.$options.filters.escape(this.sourceLang) + ',', ' target: ' + this.$options.filters.escape(this.targetLang) + ',', ' format: "' + (this.isHtml ? "html" : "text") + '",', ' api_key: "' + (localStorage.getItem("api_key") || "") + '"', ' }),', ' headers: { "Content-Type": "application/json" }', '});', '', 'console.log(await res.json());'].join("\n"); }, supportedFilesFormatFormatted: function() { return this.supportedFilesFormat.join(', '); }, isHtml: function(){ return htmlRegex.test(this.inputText); }, canSendSuggestion: function(){ return this.translatedText.trim() !== "" && this.translatedText !== this.savedTanslatedText; }, targetLangs: function(){ if (!this.sourceLang) return this.langs; else{ var lang = this.langs.find(l => l.code === this.sourceLang); if (!lang) return this.langs; var tgtLangs = lang.targets.map(t => this.langs.find(l => l.code === t)); tgtLangs.sort(function(a, b){ return a.name.localeCompare(b.name); }); return tgtLangs; } } }, filters: { escape: function(v){ return JSON.stringify(v); }, highlight: function(v){ return Prism.highlight(v, Prism.languages.javascript, 'javascript'); } }, methods: { abortPreviousTransRequest: function(){ if (this.transRequest){ this.transRequest.abort(); this.transRequest = null; } }, swapLangs: function(e){ this.closeSuggestTranslation(e); // Make sure that we can swap // by checking that the current target language // has source language as target var tgtLang = this.langs.find(l => l.code === this.targetLang); if (tgtLang.targets.indexOf(this.sourceLang) === -1) return; // Not supported var t = this.sourceLang; this.sourceLang = this.targetLang; this.targetLang = t; this.inputText = this.translatedText; this.translatedText = ""; this.handleInput(e); }, dismissError: function(){ this.error = ''; }, getQueryParam: function (key) { const params = new URLSearchParams(window.location.search); return params.get(key) }, updateQueryParam: function (key, value) { let searchParams = new URLSearchParams(window.location.search) searchParams.set(key, value); let newRelativePathQuery = window.location.pathname + '?' + searchParams.toString(); history.pushState(null, '', newRelativePathQuery); }, handleInput: function(e){ this.closeSuggestTranslation(e) this.updateQueryParam('source', this.sourceLang) this.updateQueryParam('target', this.targetLang) this.updateQueryParam('q', this.inputText) if (this.timeout) clearTimeout(this.timeout); this.timeout = null; this.detectedLangText = ""; if (this.inputText === ""){ this.translatedText = ""; this.output = ""; this.abortPreviousTransRequest(); this.loadingTranslation = false; return; } var self = this; self.loadingTranslation = true; this.timeout = setTimeout(function(){ self.abortPreviousTransRequest(); var request = new XMLHttpRequest(); self.transRequest = request; var data = new FormData(); data.append("q", self.inputText); data.append("source", self.sourceLang); data.append("target", self.targetLang); data.append("format", self.isHtml ? "html" : "text"); data.append("api_key", localStorage.getItem("api_key") || ""); if (self.apiSecret) data.append("secret", self.apiSecret); request.open('POST', BaseUrl + '/translate', true); request.onload = function() { try{ {% if api_secret != "" %} if (this.status === 400){ if (self.refreshOnce()) return; } {% endif %} var res = JSON.parse(this.response); // Success! if (res.translatedText !== undefined){ self.translatedText = res.translatedText; self.loadingTranslation = false; self.output = JSON.stringify(res, null, 4); if(self.sourceLang == "auto" && res.detectedLanguage !== undefined){ let lang = self.langs.find(l => l.code === res.detectedLanguage.language) self.detectedLangText = ": " + (lang !== undefined ? lang.name : res.detectedLanguage.language) + " (" + res.detectedLanguage.confidence + "%)"; } } else{ throw new Error(res.error || {{ _e("Unknown error") }}); } } catch (e) { self.error = e.message; self.loadingTranslation = false; } }; request.onerror = function() { self.error = {{ _e("Cannot load %(url)s", url="/translate") }}; self.loadingTranslation = false; }; request.send(data); }, self.frontendTimeout); }, copyText: function(e){ e.preventDefault(); this.$refs.translatedTextarea.select(); this.$refs.translatedTextarea.setSelectionRange(0, 9999999); /* For mobile devices */ document.execCommand("copy"); if (this.copyTextLabel === {{ _e("Copy text") }}){ this.copyTextLabel = {{ _e("Copied") }}; var self = this; setTimeout(function(){ self.copyTextLabel = {{ _e("Copy text") }}; }, 1500); } }, suggestTranslation: function(e) { e.preventDefault(); this.savedTanslatedText = this.translatedText this.isSuggesting = true; this.$nextTick(() => { this.$refs.translatedTextarea.focus(); }); }, closeSuggestTranslation: function(e) { if(this.isSuggesting) { e.preventDefault(); // this.translatedText = this.savedTanslatedText } this.isSuggesting = false; }, sendSuggestion: function(e) { e.preventDefault(); var self = this; var request = new XMLHttpRequest(); self.transRequest = request; var data = new FormData(); data.append("q", self.inputText); data.append("s", self.translatedText); data.append("source", self.sourceLang); data.append("target", self.targetLang); data.append("api_key", localStorage.getItem("api_key") || ""); request.open('POST', BaseUrl + '/suggest', true); request.onload = function() { try{ var res = JSON.parse(this.response); if (res.success){ M.toast({html: {{ _e("Thanks for your correction. Note the suggestion will not take effect right away.") }} }) self.closeSuggestTranslation(e) }else{ throw new Error(res.error || {{ _e("Unknown error") }}); } }catch(e){ self.error = e.message; self.closeSuggestTranslation(e) } }; request.onerror = function() { self.error = {{ _e("Cannot load %(url)s", url="/suggest") }}; self.loadingTranslation = false; }; request.send(data); }, deleteText: function(e){ e.preventDefault(); this.inputText = this.translatedText = this.output = ""; this.$refs.inputTextarea.focus(); }, switchType: function(type) { this.translationType = type; }, handleInputFile: function(e) { this.inputFile = e.target.files[0]; }, removeFile: function(e) { e.preventDefault() this.inputFile = false; this.translatedFileUrl = false; this.loadingFileTranslation = false; }, refreshOnce: function(){ var lastRefreshed = parseInt(localStorage.getItem("refreshed") || 0); var now = new Date().getTime(); if (now - lastRefreshed > 1000 * 60 * 1){ localStorage.setItem("refreshed", now); window.location.reload(); return true; } }, translateFile: function(e) { e.preventDefault(); let self = this; let translateFileRequest = new XMLHttpRequest(); translateFileRequest.open("POST", BaseUrl + "/translate_file", true); let data = new FormData(); data.append("file", this.inputFile); data.append("source", this.sourceLang); data.append("target", this.targetLang); data.append("api_key", localStorage.getItem("api_key") || ""); if (self.apiSecret) data.append("secret", self.apiSecret); this.loadingFileTranslation = true translateFileRequest.onload = function() { if (translateFileRequest.readyState === 4 && translateFileRequest.status === 200) { try{ {% if api_secret != "" %} if (this.status === 400){ if (self.refreshOnce()) return; } {% endif %} self.loadingFileTranslation = false; let res = JSON.parse(this.response); if (res.translatedFileUrl){ self.translatedFileUrl = res.translatedFileUrl; let link = document.createElement("a"); link.href = self.translatedFileUrl; link.download = ""; link.click(); handleNotification("Translation Complete", "Finished translating " + self.inputFile.name + "."); }else{ throw new Error(res.error || {{ _e("Unknown error") }}); } }catch(e){ self.error = e.message; self.loadingFileTranslation = false; self.inputFile = false; handleNotification("Translation Failed", e.message); } }else{ let res = JSON.parse(this.response); self.error = res.error || {{ _e("Unknown error") }}; self.loadingFileTranslation = false; self.inputFile = false; handleNotification("Translation Failed", self.error); } } translateFileRequest.onerror = function() { const message = {{ _e("Cannot load %(url)s", url="/translate_file") }}; self.error = message; self.loadingFileTranslation = false; self.inputFile = false; handleNotification("Translation Failed", message); }; translateFileRequest.send(data); Notification.requestPermission(); } } }); }); /** * @param {object} self * @param {XMLHttpRequest} response */ function handleLangsResponse(self, response) { if (response.status >= 200 && response.status < 400) { self.langs = JSON.parse(response.response); if (self.langs.length === 0){ self.loading = false; self.error = {{ _e("No languages available. Did you install the models correctly?") }}; return; } self.langs.push({ name: {{ _e("Auto Detect") }}, code: "auto", targets: self.langs.map(l => l.code)}) const sourceLanguage = self.langs.find(l => l.code === self.getQueryParam("source")) const targetLanguage = self.langs.find(l => l.code === self.getQueryParam("target")) if (sourceLanguage) { self.sourceLang = sourceLanguage.code } if (targetLanguage) { self.targetLang = targetLanguage.code } const defaultText = self.getQueryParam("q") if (defaultText) { self.inputText = defaultText self.handleInput(new Event('none')) } } else { self.error = {{ _e("Cannot load %(url)s", url="/languages") }}; } self.loading = false; } /** * @param {string} title * @param {string} body */ function handleNotification(title, body) { new Notification(title, { body, tag: 'libretranslate' }); } /** * @param {object} langDropdown * @param {string} lang */ function updateSelectedAttribute(langDropdown, lang) { for (const child of langDropdown.children) { if (child.value === lang){ child.setAttribute('selected', ''); langDropdown.style.width = getTextWidth(child.text) + 24 + 'px'; } else{ child.removeAttribute('selected'); } } } function getTextWidth(text) { var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas")); var ctx = canvas.getContext("2d"); ctx.font = 'bold 16px sans-serif'; var textWidth = Math.ceil(ctx.measureText(text).width); return textWidth; } function setApiKey(){ var prevKey = localStorage.getItem("api_key") || ""; var newKey = ""; newKey = window.prompt({{ _e("Type in your API Key. If you need an API key, %(instructions)s", instructions=_("press the \"Get API Key\" link.") if get_api_key_link else _("contact the server operator.")) }}, prevKey); if (newKey === null) newKey = ""; localStorage.setItem("api_key", newKey); } // Color scheme handling function getPreferredColorScheme(){ var systemScheme = 'light'; if(window.matchMedia('(prefers-color-scheme: dark)').matches){ systemScheme = 'dark'; } var chosenScheme = systemScheme; if(localStorage.getItem("scheme")){ chosenScheme = localStorage.getItem("scheme"); } if(systemScheme === chosenScheme){ localStorage.removeItem("scheme"); } return chosenScheme; } // Write chosen color scheme to local storage // Unless the system scheme matches the the stored scheme, in which case... remove from local storage function savePreferredColorScheme(scheme){ var systemScheme = 'light'; if(window.matchMedia('(prefers-color-scheme: dark)').matches){ systemScheme = 'dark'; } if(systemScheme === scheme){ localStorage.removeItem("scheme"); } else { localStorage.setItem("scheme", scheme); } } // Get the current scheme, and apply the opposite function toggleColorScheme(){ let newScheme = "light"; let scheme = getPreferredColorScheme(); if (scheme === "light"){ newScheme = "dark"; } applyPreferredColorScheme(newScheme); } // Apply the chosen color scheme by traversing stylesheet rules, and applying a medium. function applyPreferredColorScheme(scheme) { for (var s = 0; s < document.styleSheets.length; s++) { for (var i = 0; i < document.styleSheets[s].cssRules.length; i++) { rule = document.styleSheets[s].cssRules[i]; if (rule && rule.media && rule.media.mediaText.includes("prefers-color-scheme")) { switch (scheme) { case "light": rule.media.appendMedium("original-prefers-color-scheme"); if (rule.media.mediaText.includes("light")) rule.media.deleteMedium("(prefers-color-scheme: light)"); if (rule.media.mediaText.includes("dark")) rule.media.deleteMedium("(prefers-color-scheme: dark)"); break; case "dark": rule.media.appendMedium("(prefers-color-scheme: light)"); rule.media.appendMedium("(prefers-color-scheme: dark)"); if (rule.media.mediaText.includes("original")) rule.media.deleteMedium("original-prefers-color-scheme"); break; default: rule.media.appendMedium("(prefers-color-scheme: dark)"); if (rule.media.mediaText.includes("light")) rule.media.deleteMedium("(prefers-color-scheme: light)"); if (rule.media.mediaText.includes("original")) rule.media.deleteMedium("original-prefers-color-scheme"); break; } } } } savePreferredColorScheme(scheme); } applyPreferredColorScheme(getPreferredColorScheme()); // @license-end