mirror of
https://codeberg.org/ashley/poke
synced 2025-05-30 02:59:43 +00:00
397 lines
14 KiB
Plaintext
397 lines
14 KiB
Plaintext
<!--
|
|
This Source Code Form is subject to the terms of the GNU General Public License:
|
|
|
|
Copyright (C) 2021-2024 PokeTube (https://codeberg.org/Ashley/poketube)
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see https://www.gnu.org/licenses/.
|
|
-->
|
|
|
|
<% const languageOptions = [
|
|
{ code: 'autodetect', name: 'Autodetect' },
|
|
{ code: 'af', name: 'Afrikaans' },
|
|
{ code: 'sq', name: 'Albanian' },
|
|
{ code: 'am', name: 'Amharic' },
|
|
{ code: 'ar', name: 'Arabic' },
|
|
{ code: 'hy', name: 'Armenian' },
|
|
{ code: 'as', name: 'Assamese' },
|
|
{ code: 'ay', name: 'Aymara' },
|
|
{ code: 'az', name: 'Azerbaijani' },
|
|
{ code: 'bm', name: 'Bambara' },
|
|
{ code: 'eu', name: 'Basque' },
|
|
{ code: 'be', name: 'Belarusian' },
|
|
{ code: 'bn', name: 'Bengali' },
|
|
{ code: 'bh', name: 'Bhojpuri' },
|
|
{ code: 'bs', name: 'Bosnian' },
|
|
{ code: 'bg', name: 'Bulgarian' },
|
|
{ code: 'ca', name: 'Catalan' },
|
|
{ code: 'ceb', name: 'Cebuano' },
|
|
{ code: 'ny', name: 'Chichewa' },
|
|
{ code: 'zh-cn', name: 'Chinese (Simplified)' },
|
|
{ code: 'zh-tw', name: 'Chinese (Traditional)' },
|
|
{ code: 'co', name: 'Corsican' },
|
|
{ code: 'hr', name: 'Croatian' },
|
|
{ code: 'cs', name: 'Czech' },
|
|
{ code: 'da', name: 'Danish' },
|
|
{ code: 'dv', name: 'Dhivehi' },
|
|
{ code: 'doi', name: 'Dogri' },
|
|
{ code: 'nl', name: 'Dutch' },
|
|
{ code: 'en', name: 'English' },
|
|
{ code: 'eo', name: 'Esperanto' },
|
|
{ code: 'et', name: 'Estonian' },
|
|
{ code: 'ee', name: 'Ewe' },
|
|
{ code: 'tl', name: 'Filipino' },
|
|
{ code: 'fi', name: 'Finnish' },
|
|
{ code: 'fr', name: 'French' },
|
|
{ code: 'fy', name: 'Frisian' },
|
|
{ code: 'gl', name: 'Galician' },
|
|
{ code: 'ka', name: 'Georgian' },
|
|
{ code: 'de', name: 'German' },
|
|
{ code: 'el', name: 'Greek' },
|
|
{ code: 'gn', name: 'Guarani' },
|
|
{ code: 'gu', name: 'Gujarati' },
|
|
{ code: 'ht', name: 'Haitian Creole' },
|
|
{ code: 'ha', name: 'Hausa' },
|
|
{ code: 'haw', name: 'Hawaiian' },
|
|
{ code: 'he', name: 'Hebrew' },
|
|
{ code: 'hi', name: 'Hindi' },
|
|
{ code: 'hmn', name: 'Hmong' },
|
|
{ code: 'hu', name: 'Hungarian' },
|
|
{ code: 'is', name: 'Icelandic' },
|
|
{ code: 'ig', name: 'Igbo' },
|
|
{ code: 'ilo', name: 'Ilocano' },
|
|
{ code: 'id', name: 'Indonesian' },
|
|
{ code: 'ga', name: 'Irish' },
|
|
{ code: 'it', name: 'Italian' },
|
|
{ code: 'ja', name: 'Japanese' },
|
|
{ code: 'jv', name: 'Javanese' },
|
|
{ code: 'kn', name: 'Kannada' },
|
|
{ code: 'kk', name: 'Kazakh' },
|
|
{ code: 'km', name: 'Khmer' },
|
|
{ code: 'rw', name: 'Kinyarwanda' },
|
|
{ code: 'kok', name: 'Konkani' },
|
|
{ code: 'ko', name: 'Korean (PROK)' },
|
|
{ code: 'kri', name: 'Krio' },
|
|
{ code: 'ku', name: 'Kurdish (Kurmanji)' },
|
|
{ code: 'sd', name: 'Sindhi' },
|
|
{ code: 'si', name: 'Sinhala' },
|
|
{ code: 'sk', name: 'Slovak' },
|
|
{ code: 'sl', name: 'Slovenian' },
|
|
{ code: 'so', name: 'Somali' },
|
|
{ code: 'es', name: 'Spanish' },
|
|
{ code: 'su', name: 'Sundanese' },
|
|
{ code: 'sw', name: 'Swahili' },
|
|
{ code: 'sv', name: 'Swedish' },
|
|
{ code: 'tg', name: 'Tajik' },
|
|
{ code: 'ta', name: 'Tamil' },
|
|
{ code: 'tt', name: 'Tatar' },
|
|
{ code: 'te', name: 'Telugu' },
|
|
{ code: 'th', name: 'Thai' },
|
|
{ code: 'ti', name: 'Tigrinya' },
|
|
{ code: 'ts', name: 'Tsonga' },
|
|
{ code: 'tr', name: 'Turkish' },
|
|
{ code: 'tk', name: 'Turkmen' },
|
|
{ code: 'twi', name: 'Twi' },
|
|
{ code: 'uk', name: 'Ukrainian' },
|
|
{ code: 'ur', name: 'Urdu' },
|
|
{ code: 'ug', name: 'Uyghur' },
|
|
{ code: 'uz', name: 'Uzbek' },
|
|
{ code: 'vi', name: 'Vietnamese' },
|
|
{ code: 'cy', name: 'Welsh' },
|
|
{ code: 'xh', name: 'Xhosa' },
|
|
{ code: 'yi', name: 'Yiddish' },
|
|
{ code: 'yo', name: 'Yoruba' },
|
|
{ code: 'zu', name: 'Zulu' }
|
|
]; %>
|
|
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<title>PokeTranslate</title>
|
|
<link rel="icon" href="/static/yt-ukraine.svg">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta content="PokeTranslate" property=og:title>
|
|
<meta content="Translate text - Anonymously!" property=twitter:description>
|
|
<meta content="https://cdn.glitch.global/d68d17bb-f2c0-4bc3-993f-50902734f652/aa70111e-5bcd-4379-8b23-332a33012b78.image.png?v=1701898829884" property="og:image" />
|
|
<meta content=summary_large_image name=twitter:card>
|
|
<meta charset="UTF-8">
|
|
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'">
|
|
<meta name="referrer" content="no-referrer">
|
|
<link rel="manifest" href="/manifest.json">
|
|
<link href="https://fonts.bunny.net/css?family=poppins:400,500,600" rel="stylesheet">
|
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
|
|
|
<style>
|
|
:root {
|
|
--accent-1: #ff6b6b;
|
|
--accent-2: #f7d794;
|
|
--bg-light: #fafafa;
|
|
--bg-dark: #202027;
|
|
--card-light: rgba(255,255,255,0.55);
|
|
--card-dark: rgba(18,18,24,0.55);
|
|
--radius: 20px;
|
|
--transition: 0.3s ease;
|
|
}
|
|
* { margin:0; padding:0; box-sizing:border-box; }
|
|
body {
|
|
font-family:'Poppins',sans-serif;
|
|
background: var(--bg-light);
|
|
color: #fff;
|
|
display:flex; align-items:center; justify-content:center;
|
|
min-height:100vh; padding:20px;
|
|
transition: background var(--transition), color var(--transition);
|
|
overflow:hidden;
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
body { background: var(--bg-dark); }
|
|
}
|
|
|
|
.glass {
|
|
position: relative;
|
|
background: var(--card-light);
|
|
backdrop-filter: blur(30px) saturate(200%);
|
|
border-radius: var(--radius);
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
|
width:100%; max-width:840px; overflow:hidden;
|
|
transition: background var(--transition), box-shadow var(--transition);
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
.glass {
|
|
background: var(--card-dark);
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.7);
|
|
}
|
|
}
|
|
|
|
.blob {
|
|
position:absolute; width:300px; height:300px;
|
|
background: linear-gradient(45deg,var(--accent-1),var(--accent-2));
|
|
border-radius: 45% 55% 70% 30%/30% 60% 40% 70%;
|
|
animation: blobMove 8s infinite; opacity:0.3; z-index:0;
|
|
}
|
|
.blob:nth-child(1){ top:-80px; left:-60px; }
|
|
.blob:nth-child(2){ bottom:-100px; right:-80px; animation-duration:10s; }
|
|
@keyframes blobMove {
|
|
0%,100% { transform:translate(0,0) scale(1); }
|
|
50% { transform:translate(20px,20px) scale(1.1); }
|
|
}
|
|
|
|
header {
|
|
position:relative; padding:28px; text-align:center;
|
|
background: linear-gradient(60deg,var(--accent-1),var(--accent-2));
|
|
background-size:200% 200%; animation:gradientAnimation 6s ease infinite;
|
|
z-index:1;
|
|
}
|
|
@keyframes gradientAnimation {
|
|
0%{ background-position:0% 50%; }
|
|
50%{ background-position:100% 50%; }
|
|
100%{ background-position:0% 50%; }
|
|
}
|
|
|
|
/* Rainbow title unless user prefers reduced motion */
|
|
@media (prefers-reduced-motion: no-preference) {
|
|
header h1 {
|
|
font-size:2.4rem; font-weight:600; letter-spacing:1.5px;
|
|
background: conic-gradient(red,orange,yellow,green,blue,indigo,violet,red);
|
|
background-size:300%;
|
|
-webkit-background-clip:text;
|
|
-webkit-text-fill-color:transparent;
|
|
animation: rainbow 4s linear infinite;
|
|
}
|
|
@keyframes rainbow {
|
|
0% { background-position: 0% 50%; }
|
|
50% { background-position: 100% 50%; }
|
|
100% { background-position: 0% 50%; }
|
|
}
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
header h1 {
|
|
font-size:2.4rem; font-weight:600; letter-spacing:1.5px;
|
|
color:#fff;
|
|
}
|
|
}
|
|
|
|
form { position:relative; z-index:2; }
|
|
.language-bar { display:flex; align-items:center; gap:12px; padding:16px 24px; }
|
|
.language-select { flex:1; }
|
|
.language-select select {
|
|
width:100%; padding:14px 18px; border-radius:var(--radius);
|
|
border:1px solid rgba(255,255,255,0.5); background:transparent;
|
|
font-size:1rem; appearance:none; cursor:pointer; color:#fff;
|
|
transition:border-color var(--transition);
|
|
}
|
|
.language-select select:focus { border-color:var(--accent-1); outline:none; }
|
|
.swap-button {
|
|
font-size:2rem; color:var(--accent-1); text-decoration:none;
|
|
transition:transform var(--transition),color var(--transition);
|
|
}
|
|
.swap-button:hover {
|
|
transform:rotate(180deg) scale(1.1);
|
|
color:var(--accent-2);
|
|
}
|
|
|
|
.panels {
|
|
display:grid; grid-template-columns:1fr 1fr; gap:20px;
|
|
padding:0 24px 24px;
|
|
}
|
|
.panel { position:relative; }
|
|
.panel label {
|
|
display:block; margin-bottom:8px; font-weight:500; font-size:0.95rem;
|
|
color:#fff;
|
|
}
|
|
.panel textarea {
|
|
width:100%; min-height:160px; padding:16px; border-radius:var(--radius);
|
|
border:1px solid rgba(255,255,255,0.5); resize:vertical; font-size:1rem;
|
|
line-height:1.5; background:rgba(255,255,255,0.2); color:#fff;
|
|
transition:border-color var(--transition),box-shadow var(--transition);
|
|
}
|
|
.panel textarea::placeholder { color:rgba(255,255,255,0.7); }
|
|
@media (prefers-color-scheme: dark) {
|
|
.panel textarea { background:rgba(20,20,25,0.5); }
|
|
}
|
|
.panel textarea:focus {
|
|
border-color:var(--accent-1);
|
|
box-shadow:0 0 8px var(--accent-1);
|
|
outline:none;
|
|
}
|
|
|
|
.actions { display:flex; justify-content:center; padding-bottom:24px; }
|
|
.actions button {
|
|
padding:16px 36px; font-size:1.1rem; font-weight:600; border:none;
|
|
border-radius:var(--radius); cursor:pointer;
|
|
background:linear-gradient(60deg,var(--accent-1),var(--accent-2));
|
|
background-size:200% 200%; animation:gradientAnimation 6s ease infinite;
|
|
color:#fff; box-shadow:0 4px 14px rgba(0,0,0,0.2);
|
|
transition:transform var(--transition),opacity var(--transition);
|
|
}
|
|
.actions button:hover { transform:scale(1.03); opacity:0.9; }
|
|
|
|
@media (max-width:768px) { .panels { grid-template-columns:1fr; } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="glass">
|
|
<div class="blob"></div>
|
|
<div class="blob"></div>
|
|
|
|
<header><h1>PokeTranslate</h1></header>
|
|
|
|
<form action="/translate" method="GET">
|
|
<div class="language-bar">
|
|
<div class="language-select">
|
|
<select name="from_language" id="from_language">
|
|
<% languageOptions.forEach(lang => { %>
|
|
<option value="<%= lang.code %>" <%= lang.code === (from_language||'autodetect')?'selected':''%>>
|
|
<%= lang.name %>
|
|
</option>
|
|
<% }); %>
|
|
</select>
|
|
</div>
|
|
|
|
<a
|
|
href="?from_language=<%= to_language %>&to_language=<%= from_language %>&input=<%= encodeURIComponent(text) %>"
|
|
id="swapBtn"
|
|
class="material-icons swap-button"
|
|
>swap_horiz</a>
|
|
|
|
<div class="language-select">
|
|
<select name="to_language" id="to_language">
|
|
<% languageOptions.slice(1).forEach(lang => { %>
|
|
<option value="<%= lang.code %>" <%= lang.code===to_language?'selected':''%>>
|
|
<%= lang.name %>
|
|
</option>
|
|
<% }); %>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panels">
|
|
<div class="panel input">
|
|
<label for="input">Input</label>
|
|
<textarea id="input" name="input" placeholder="Enter text…"><%= text %></textarea>
|
|
</div>
|
|
<div class="panel output">
|
|
<label for="output">Translation</label>
|
|
<textarea id="output" readonly placeholder="Translated text…"><%= translation %></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="actions">
|
|
<button type="submit">Translate :3</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<script>
|
|
(function(){
|
|
// swap button
|
|
const swapBtn = document.getElementById('swapBtn');
|
|
if(swapBtn){
|
|
swapBtn.addEventListener('click', function(e){
|
|
e.preventDefault();
|
|
const from = document.getElementById('from_language');
|
|
const to = document.getElementById('to_language');
|
|
const inp = document.getElementById('input');
|
|
const out = document.getElementById('output');
|
|
[from.value, to.value] = [to.value, from.value];
|
|
[inp.value, out.value] = [out.value, inp.value];
|
|
});
|
|
}
|
|
|
|
// auto-resize all textareas
|
|
document.querySelectorAll('textarea').forEach(el=>{
|
|
const resize = ()=>{ el.style.height='auto'; el.style.height=el.scrollHeight+'px'; };
|
|
el.addEventListener('input', resize);
|
|
resize();
|
|
});
|
|
|
|
// live-translate if JS available
|
|
if(window.fetch){
|
|
const from = document.getElementById('from_language');
|
|
const to = document.getElementById('to_language');
|
|
const input = document.getElementById('input');
|
|
const output= document.getElementById('output');
|
|
let timer;
|
|
|
|
function translateNow(){
|
|
const q = new URLSearchParams({
|
|
from_language: from.value,
|
|
to_language: to.value,
|
|
input: input.value
|
|
});
|
|
fetch(`/translate?${q}`)
|
|
.then(r => r.text())
|
|
.then(html => {
|
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
const newOut = doc.getElementById('output');
|
|
if(newOut){
|
|
output.value = newOut.value;
|
|
output.dispatchEvent(new Event('input'));
|
|
}
|
|
})
|
|
.catch(() => {/* ignore */});
|
|
}
|
|
|
|
// debounce on typing
|
|
input.addEventListener('input', ()=>{
|
|
clearTimeout(timer);
|
|
timer = setTimeout(translateNow, 500);
|
|
});
|
|
// re-translate on language change
|
|
from.addEventListener('change', translateNow);
|
|
to.addEventListener('change', translateNow);
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|