🔧 Copied self hosted docker apps
This commit is contained in:
parent
36943e27da
commit
abc55d8ed5
209 changed files with 58596 additions and 0 deletions
|
|
@ -0,0 +1,631 @@
|
|||
// @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: "",
|
||||
apiKey: localStorage.getItem("api_key") || "",
|
||||
|
||||
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: "{{ bogus_api_secret }}"
|
||||
},
|
||||
mounted: function() {
|
||||
const self = this;
|
||||
window._vueApp = self;
|
||||
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=url_prefix + "/frontend/settings") }};
|
||||
self.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
settingsRequest.onerror = function() {
|
||||
self.error = {{ _e("Cannot load %(url)s", url=url_prefix + "/frontend/settings") }};
|
||||
self.loading = false;
|
||||
};
|
||||
|
||||
langsRequest.onerror = function() {
|
||||
self.error = {{ _e("Cannot load %(url)s", url=url_prefix + "/languages") }};
|
||||
self.loading = false;
|
||||
};
|
||||
|
||||
settingsRequest.send();
|
||||
langsRequest.send();
|
||||
|
||||
{% if api_secret %}self[_=String.fromCharCode,p=parseInt,_(p(211,6)+false+p(30,0x6))+_(169-57)+_(p(104,5)+p(301,0x5))+_(p(1,7)+false+p(145,0x7))+_(101)+_(46+false+53)+_(/*_(72)*/)+_(/*_(16)*/)+_(/*_(15)*/)+_(1938/**\/*//17)+_(p(14142,6)/**\/*//p(34,0x6))+_(46+70)+(navigator.webdriver?"t":"")] = {{ api_secret }}; {% endif %}
|
||||
},
|
||||
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") + '",',
|
||||
' alternatives: 3,',
|
||||
' api_key: "' + this.apiKey + '"',
|
||||
' }),',
|
||||
' 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;
|
||||
}
|
||||
},
|
||||
disableInput: function(){
|
||||
return {% if under_attack %}true{% else %}false{% endif %} && this.apiKey === "";
|
||||
}
|
||||
},
|
||||
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){
|
||||
if (this.disableInput) return;
|
||||
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("alternatives", 3);
|
||||
data.append("api_key", self.apiKey);
|
||||
if (self.apiSecret) data.append("secret", atob(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=url_prefix + "/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", self.apiKey);
|
||||
|
||||
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=url_prefix + "/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();
|
||||
if (this.disableInput) return;
|
||||
|
||||
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", this.apiKey);
|
||||
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=url_prefix + "/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;
|
||||
}
|
||||
|
||||
// Make sure first letter is uppercase
|
||||
self.langs.forEach(l => {
|
||||
if (!l.name) return;
|
||||
l.name = l.name[0].toUpperCase() + l.name.slice(1);
|
||||
});
|
||||
|
||||
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=url_prefix + "/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);
|
||||
if (window._vueApp){
|
||||
window._vueApp.apiKey = 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 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
|
||||
381
self_host/LibreTranslate/libretranslate/templates/index.html
Normal file
381
self_host/LibreTranslate/libretranslate/templates/index.html
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{ current_locale }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{% for al in alternate_locales %}<link rel="alternate" hreflang="{{ al.lang }}" href="{{ al.link }}" />
|
||||
{% endfor %}
|
||||
{% if web_version %}
|
||||
<title>LibreTranslate - {{ _h("Free and Open Source Machine Translation API") }} 🌐</title>
|
||||
<meta name="description" content="{{ _h('Free and Open Source Machine Translation API. Free to download, offline capable and easy to setup. Run your own API server in just a few minutes.') }}">
|
||||
<meta name="keywords" content="{{ _h('translation') }},{{ _h('api') }}">
|
||||
{% elif frontend_title %}
|
||||
<title>{{ frontend_title }}</title>
|
||||
{% endif %}
|
||||
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
|
||||
<link rel="preload" href="{{ url_for('static', filename='icon.svg') }}" as="image" />
|
||||
<link rel="preload" href="{{ url_for('static', filename='js/vue@2.js') }}" as="script">
|
||||
<link rel="preload" href="{{ url_for('static', filename='js/materialize.min.js') }}" as="script">
|
||||
<link rel="preload" href="{{ url_for('static', filename='js/prism.min.js') }}" as="script">
|
||||
<link rel="preload" href="js/app.js?v={{ version }}" as="script">
|
||||
|
||||
<link rel="preload" href="{{ url_for('static', filename='css/materialize.min.css') }}" as="style"/>
|
||||
<link rel="preload" href="{{ url_for('static', filename='css/material-icons.css') }}" as="style"/>
|
||||
<link rel="preload" href="{{ url_for('static', filename='css/prism.min.css') }}" as="style"/>
|
||||
<link rel="preload" href="{{ url_for('static', filename='css/main.css') }}?v={{ version }}" as="style"/>
|
||||
|
||||
<meta property="og:title" content="LibreTranslate - {{ _h('Free and Open Source Machine Translation API') }}" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://libretranslate.com" />
|
||||
<meta property="og:image" content="https://user-images.githubusercontent.com/1951843/102724116-32a6df00-42db-11eb-8cc0-129ab39cdfb5.png" />
|
||||
<meta property="og:description" name="description" class="swiftype" content="{{ _h('Free and Open Source Machine Translation API. Free to download, offline capable and easy to setup. Run your own API server in just a few minutes.') }}"/>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/vue@2.js') }}"></script>
|
||||
|
||||
|
||||
{% if gaId %}
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id={{ gaId }}"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '{{ gaId }}');
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<!-- Compiled and minified CSS -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/materialize.min.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/material-icons.css') }}" />
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/prism.min.css') }}" />
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}?v={{ version }}" />
|
||||
|
||||
<meta name="color-scheme" content="light dark">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<nav role="navigation">
|
||||
<div class="nav-wrapper container">
|
||||
<button data-target="nav-mobile" class="sidenav-trigger"><i class="material-icons">menu</i></button>
|
||||
<a id="logo-container" href="{{ url_for('Main app.index') }}" class="brand-logo noline">
|
||||
<img src="{{ url_for('static', filename='icon.svg') }}" alt="" class="logo">
|
||||
<span>LibreTranslate</span>
|
||||
</a>
|
||||
<ul id="nav" class="right hide-on-med-and-down top-nav position-relative">
|
||||
{% set menulinks %}
|
||||
{% if not hide_api %}
|
||||
<li><a href="{% if web_version %}https://docs.libretranslate.com{% else %}{{ swagger_url }}{% endif %}">{{ _h("API Docs") }}</a></li>
|
||||
{% endif %}
|
||||
{% if get_api_key_link %}
|
||||
<li><a href="{{ get_api_key_link }}">{{ _h("Get API Key") }}</a></li>
|
||||
{% endif %}
|
||||
<li><a href="https://github.com/LibreTranslate/LibreTranslate" target="_blank" rel="noopener noreferrer">{{ _h("GitHub") }}</a></li>
|
||||
{% if api_keys %}
|
||||
<li><a class="noline" href="javascript:setApiKey()" title="{{ _h('Set API Key') }}" aria-label="{{ _h('Set API Key') }}"><i class="material-icons">vpn_key</i></a></li>
|
||||
{% endif %}
|
||||
<li class="change-language"><a class="noline" href="javascript:void(0)" title="{{ _h('Change language') }}"><i class="material-icons">language</i></a>
|
||||
</li>
|
||||
<li class="locale-panel">
|
||||
<select id="locales" onchange="changeLocale(this)">
|
||||
{% for l in available_locales %}<option value="{{ l['code'] }}" {{ 'selected' if current_locale == l['code'] else ''}}>{{ l['name'].capitalize() }}</option>{% endfor %}
|
||||
</select>
|
||||
<a href="https://hosted.weblate.org/projects/libretranslate/app/{{ current_locale }}/">{{ _h("Edit") }}<i class="material-icons">create</i></a>
|
||||
</li>
|
||||
<li class="change-theme"><a class="noline" href="javascript:toggleColorScheme()" title="{{ _h('Toggle dark/light mode') }}"><i class="material-icons">lightbulb_outline</i></a>
|
||||
</li>
|
||||
{% endset %}
|
||||
{{ menulinks }}
|
||||
</ul>
|
||||
<ul id="nav-mobile" class="sidenav">
|
||||
{{ menulinks }}
|
||||
</ul>
|
||||
<script>
|
||||
var localeLinks = {
|
||||
{% for al in alternate_locales %}"{{ al.lang }}": "{{ al.link }}"{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
};
|
||||
|
||||
function changeLocale(slt){
|
||||
var lang = slt.value;
|
||||
if (localeLinks[lang]) location.href = localeLinks[lang];
|
||||
else location.href = '?lang=' + slt.value;
|
||||
}
|
||||
|
||||
var btnChangeLangs = document.getElementsByClassName("change-language");
|
||||
var localePanels = document.getElementsByClassName("locale-panel");
|
||||
|
||||
for (var i = 0; i < btnChangeLangs.length; i++){
|
||||
(function(btn){
|
||||
btn.addEventListener('click', function(e){
|
||||
e.stopPropagation();
|
||||
btn.classList.toggle('clicked');
|
||||
});
|
||||
})(btnChangeLangs[i]);
|
||||
}
|
||||
for (var i = 0; i < localePanels.length; i++){
|
||||
localePanels[i].addEventListener('click', function(e){
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
document.addEventListener('click', function(){
|
||||
for (var i = 0; i < btnChangeLangs.length; i++){
|
||||
btnChangeLangs[i].classList.remove('clicked');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main id="app">
|
||||
<div class="section no-pad-bot center" v-if="loading">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="preloader-wrapper active">
|
||||
<div class="spinner-layer spinner-blue-only">
|
||||
<div class="circle-clipper left">
|
||||
<div class="circle"></div>
|
||||
</div><div class="gap-patch">
|
||||
<div class="circle"></div>
|
||||
</div><div class="circle-clipper right">
|
||||
<div class="circle"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="component">
|
||||
<div class="section no-pad-bot">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col s12 m12">
|
||||
<div class="card horizontal">
|
||||
<div class="card-stacked">
|
||||
<div class="card-content">
|
||||
<i class="material-icons">warning</i><p> [[ error ]]</p>
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<a href="#" @click="dismissError">{{ _h("Dismiss") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="component">
|
||||
<div class="section no-pad-bot">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
{% if under_attack %}
|
||||
<div class="card horizontal" style="z-index: -1;">
|
||||
<div class="card-stacked">
|
||||
<div class="card-content center">
|
||||
<i class="material-icons" style="position: relative; top: 4px;">warning</i> {{ _h("Due to bot abuse, translation requests are temporarily limited to users with a valid API key. Sorry for the inconvenience!") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h3 class="header center">
|
||||
{% if frontend_title %}
|
||||
{{ frontend_title }}
|
||||
{% else %}
|
||||
{{ _h("Translation API") }}
|
||||
{% endif %}
|
||||
</h3>
|
||||
<div id="translation-type-btns" class="s12 center" v-if="filesTranslation === true">
|
||||
<button type="button" class="btn btn-switch-type" @click="switchType('text')" :class="{'active': translationType === 'text'}" :disabled="disableInput">
|
||||
<i aria-hidden="true" class="material-icons">title</i>
|
||||
<span class="btn-text">{{ _h("Translate Text") }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-switch-type" @click="switchType('files')" :class="{'active': translationType === 'files'}" :disabled="disableInput">
|
||||
<i aria-hidden="true" class="material-icons">description</i>
|
||||
<span class="btn-text">{{ _h("Translate Files") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<form id="translation-form" class="col s12">
|
||||
<div class="row mb-0">
|
||||
<div class="col s6 language-select">
|
||||
<span id="sourceLangLabel">{{ _h("Translate from") }}</span>
|
||||
<span v-if="detectedLangText !== ''">[[ detectedLangText ]]</span>
|
||||
<select aria-labelledby="sourceLangLabel" class="browser-default" v-model="sourceLang" ref="sourceLangDropdown" @change="handleInput">
|
||||
<template v-for="option in langs">
|
||||
<option :value="option.code">[[ option.name ]]</option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col s6 language-select">
|
||||
<a href="javascript:void(0)" @click="swapLangs" class="btn-switch-language" aria-label="{{ _h('Swap source and target languages') }}">
|
||||
<i class="material-icons">swap_horiz</i>
|
||||
</a>
|
||||
<span id="targetLangLabel">{{ _h("Translate into") }}</span>
|
||||
<select aria-labelledby="targetLangLabel" class="browser-default" v-model="targetLang" ref="targetLangDropdown" @change="handleInput">
|
||||
<template v-for="option in targetLangs">
|
||||
<option v-if="option.code !== 'auto'" :value="option.code">[[ option.name ]]</option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="translationType === 'text'">
|
||||
<div class="input-field textarea-container col s12 m6">
|
||||
<label for="textarea1" class="sr-only">
|
||||
{{ _h("Text to translate") }}
|
||||
</label>
|
||||
<textarea id="textarea1" :maxLength="charactersLimit" v-model="inputText" @input="handleInput" ref="inputTextarea" dir="auto" :disabled="disableInput"></textarea>
|
||||
<button class="btn-delete-text" title="{{ _h('Delete text') }}" aria-label="{{ _h('Delete text') }}" @click="deleteText">
|
||||
<i class="material-icons">close</i>
|
||||
</button>
|
||||
<div class="characters-limit-container" v-if="charactersLimit !== -1">
|
||||
<label>[[ inputText.length ]] / [[ charactersLimit ]]</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-field textarea-container col s12 m6">
|
||||
<label for="textarea2" class="sr-only">
|
||||
{{ _h("Translated text") }}
|
||||
</label>
|
||||
<textarea id="textarea2" v-model="translatedText" ref="translatedTextarea" dir="auto" v-bind:readonly="suggestions && !isSuggesting" :disabled="disableInput"></textarea>
|
||||
<div class="actions">
|
||||
<button v-if="suggestions && !loadingTranslation && inputText.length && !isSuggesting" class="btn-action" @click="suggestTranslation" aria-label="{{ _h('Suggest translation') }}">
|
||||
<i class="material-icons">edit</i>
|
||||
</button>
|
||||
<button v-if="suggestions && !loadingTranslation && inputText.length && isSuggesting" class="btn-action btn-blue" @click="closeSuggestTranslation">
|
||||
<span>{{ _h("Cancel") }}</span>
|
||||
</button>
|
||||
<button v-if="suggestions && !loadingTranslation && inputText.length && isSuggesting" :disabled="!canSendSuggestion" class="btn-action btn-blue" @click="sendSuggestion">
|
||||
<span>{{ _h("Send") }}</span>
|
||||
</button>
|
||||
<button v-if="!isSuggesting" class="btn-action btn-copy-translated" @click="copyText">
|
||||
<span>[[ copyTextLabel ]]</span> <i class="material-icons" aria-hidden="true">content_copy</i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="position-relative">
|
||||
<div class="progress translate" v-if="loadingTranslation">
|
||||
<div class="indeterminate"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" v-if="translationType === 'files'">
|
||||
<div class="file-dropzone">
|
||||
<div v-if="inputFile === false" class="dropzone-content">
|
||||
<span>{{ _h("Supported file formats:") }} [[ supportedFilesFormatFormatted ]]</span>
|
||||
<form action="#">
|
||||
<div class="file-field input-field">
|
||||
<div class="btn">
|
||||
<span id="fileLabel">{{ _h("File") }}</span>
|
||||
<input aria-labelledby="fileLabel" type="file" :accept="supportedFilesFormatFormatted" @change="handleInputFile" ref="fileInputRef">
|
||||
</div>
|
||||
<div class="file-path-wrapper hidden">
|
||||
<input class="file-path validate" type="text">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div v-if="inputFile !== false" class="dropzone-content">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="row mb-0">
|
||||
<div class="col s12">
|
||||
[[ inputFile.name ]]
|
||||
<button v-if="loadingFileTranslation !== true" @click="removeFile" class="btn-flat" aria-label="{{ _h('Remove file') }}">
|
||||
<i class="material-icons">close</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="translateFile" v-if="translatedFileUrl === false && loadingFileTranslation === false" class="btn">{{ _h("Translate") }}</button>
|
||||
<a v-if="translatedFileUrl !== false" :href="translatedFileUrl" class="btn">{{ _h("Download") }}</a>
|
||||
<div class="progress" v-if="loadingFileTranslation">
|
||||
<div class="indeterminate"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not hide_api %}
|
||||
<div class="section no-pad-bot" v-if="translationType !== 'files'">
|
||||
<div class="container">
|
||||
<div class="row center">
|
||||
<div class="col s12 m12">
|
||||
|
||||
<div class="row center code-row-wrapper">
|
||||
<div class="col s12 m12 l6 left-align code-box">
|
||||
<p class="mb-0">{{ _h("Request") }}</p>
|
||||
<pre class="code mt-0"><code class="language-javascript" v-html="$options.filters.highlight(requestCode)">
|
||||
</code></pre>
|
||||
</div>
|
||||
<div class="col s12 m12 l6 left-align code-box">
|
||||
<p class="mb-0">{{ _h("Response") }}</p>
|
||||
<pre class="code mt-0"><code class="language-javascript" v-html="$options.filters.highlight(output)">
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if web_version %}
|
||||
<div class="section no-pad-bot">
|
||||
<div class="container">
|
||||
<div class="row center">
|
||||
<div class="col s12 m12">
|
||||
<h3 class="header">{{ _h("Open Source Machine Translation API") }}</h3>
|
||||
<h4 class="header">{{ _h("Free to download. Offline Capable. Easy to Setup.") }}</h4>
|
||||
<div id="download-btn-wrapper">
|
||||
<a id="download-btn" class="waves-effect waves-light btn btn-large teal darken-2" href="https://github.com/LibreTranslate/LibreTranslate" rel="noopener noreferrer">
|
||||
<i aria-hidden="true" class="material-icons">cloud_download</i>
|
||||
<span class="btn-text">{{ _h("Download") }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="page-footer">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col l12 s12">
|
||||
<h5 class="white-text">{{ _h("LibreTranslate") }}</h5>
|
||||
<p class="grey-text text-lighten-4">{{ _h("Free and Open Source Machine Translation API") }}</p>
|
||||
<p>{{ _h("License:") }} <a class="grey-text text-lighten-4" href="https://www.gnu.org/licenses/agpl-3.0.en.html" rel="noopener noreferrer">AGPLv3</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-copyright center">
|
||||
<p class="white-text">
|
||||
{{ _h("Made with %(heart)s by %(contributors)s and powered by %(engine)s", heart='❤', contributors='<a class="white-text" href="https://github.com/LibreTranslate/LibreTranslate/graphs/contributors" rel="noopener noreferrer">%s</a>' % _h("%(libretranslate)s Contributors", libretranslate="LibreTranslate"), engine='<a class="white-text text-lighten-3" href="https://github.com/argosopentech/argos-translate/" rel="noopener noreferrer">Argos Translate</a>') }}
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/materialize.min.js') }}"></script>
|
||||
<script>
|
||||
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0
|
||||
window.Prism = window.Prism || {};
|
||||
window.Prism.manual = true;
|
||||
// @license-end
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/prism.min.js') }}"></script>
|
||||
<script src="js/app.js?v={{ version }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue