77 body {
88 font-family : Arial, sans-serif;
99 margin-top : 50px ;
10+ width : 1150px ;
11+ margin : auto;
12+ min-height : 100vh ;
13+ position : relative;
1014 }
1115
1216 header {
1317 display : flex;
1418 gap : 1em ;
1519 }
20+
21+ .translation-section {
22+ display : flex;
23+ justify-content : space-between;
24+ align-content : center;
25+ }
26+
27+ .source , .target {
28+ width : 45% ;
29+ display : flex;
30+ flex-direction : column;
31+ gap : 0.5em ;
32+ border : 1px solid # ccc ;
33+ padding : 1% ;
34+ }
35+
36+ .source textarea , .target textarea {
37+ width : 98% ;
38+ resize : none;
39+ }
40+
41+ .dropdown-area select {
42+ width : 200px ;
43+ }
44+
45+ .swap-button-area {
46+ display : flex;
47+ align-items : center;
48+ justify-content : center;
49+ }
50+
51+ .text-area {
52+ height : 20em ;
53+ }
54+
55+ .text-area textarea {
56+ min-height : 60% ;
57+ height : 60% ;
58+ }
59+ .text-area .transliterated-text {
60+ margin-top : 0.5em ;
61+ font-style : italic;
62+ color : # 555 ;
63+ max-height : 35% ;
64+ overflow-y : auto;
65+ }
66+
67+ .actions {
68+ display : flex;
69+ justify-content : flex-end;
70+ gap : 2em ;
71+ }
72+
73+ .actions button {
74+ height : 32px ;
75+ }
76+
77+ footer {
78+ position : absolute;
79+ bottom : 10px ;
80+ width : 60% ;
81+ text-align : center;
82+ margin : 0 20% ;
83+ }
1684 </ style >
1785 </ head >
1886 < body >
@@ -21,12 +89,92 @@ <h1>Glotter</h1>
2189 < h3 id ="version "> </ h3 >
2290 </ header >
2391
24- < section >
25- < h2 > Supported Languges</ h2 >
26- < ol id ="languages "> </ ol >
92+ < section class ="translation-section ">
93+ < div class ="source ">
94+ < div class ="dropdown-area ">
95+ < select id ="select-source " name ="select-source "> </ select >
96+ </ div >
97+ < div class ="text-area ">
98+ < textarea id ="source-text " name ="source-text "> </ textarea >
99+ < div id ="source-transliteration " class ="transliterated-text "> </ div >
100+ </ div >
101+ < div class ="actions ">
102+ < button id ="paste-button ">
103+ < svg xmlns ="http://www.w3.org/2000/svg " height ="24px " viewBox ="0 -960 960 960 " width ="24px " fill ="#000000 "> < path d ="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h167q11-35 43-57.5t70-22.5q40 0 71.5 22.5T594-840h166q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-560h-80v80q0 17-11.5 28.5T640-640H320q-17 0-28.5-11.5T280-680v-80h-80v560Zm280-560q17 0 28.5-11.5T520-800q0-17-11.5-28.5T480-840q-17 0-28.5 11.5T440-800q0 17 11.5 28.5T480-760Z "/>
104+ </ svg >
105+ </ button >
106+ < button id ="translate-button "> Translate</ button >
107+ </ div >
108+ </ div >
109+ < div class ="swap-button-area ">
110+ < button id ="swap-button ">
111+ < svg xmlns ="http://www.w3.org/2000/svg " height ="24px " viewBox ="0 -960 960 960 " width ="24px " fill ="#000000 "> < path d ="m233-280 76 76q12 12 11.5 28T308-148q-12 11-28 11.5T252-148L108-292q-6-6-8.5-13T97-320q0-8 2.5-15t8.5-13l144-144q11-11 27.5-11t28.5 11q12 12 12 28.5T308-435l-75 75h567q17 0 28.5 11.5T840-320q0 17-11.5 28.5T800-280H233Zm494-320H160q-17 0-28.5-11.5T120-640q0-17 11.5-28.5T160-680h567l-76-76q-12-12-11.5-28t12.5-28q12-11 28-11.5t28 11.5l144 144q6 6 8.5 13t2.5 15q0 8-2.5 15t-8.5 13L708-468q-11 11-27.5 11T652-468q-12-12-12-28.5t12-28.5l75-75Z "/>
112+ </ svg >
113+ </ button >
114+ </ div >
115+ < div class ="target ">
116+ < div class ="dropdown-area ">
117+ < select id ="select-target " name ="select-target "> </ select >
118+ </ div >
119+ < div class ="text-area ">
120+ < textarea id ="target-text " name ="target-text " disabled > </ textarea >
121+ < div id ="target-transliteration " class ="transliterated-text "> </ div >
122+ </ div >
123+ < div class ="actions ">
124+ < button id ="copy-button ">
125+ < svg xmlns ="http://www.w3.org/2000/svg " height ="24px " viewBox ="0 -960 960 960 " width ="24px " fill ="#000000 "> < path d ="M360-240q-33 0-56.5-23.5T280-320v-480q0-33 23.5-56.5T360-880h360q33 0 56.5 23.5T800-800v480q0 33-23.5 56.5T720-240H360Zm0-80h360v-480H360v480ZM200-80q-33 0-56.5-23.5T120-160v-520q0-17 11.5-28.5T160-720q17 0 28.5 11.5T200-680v520h400q17 0 28.5 11.5T640-120q0 17-11.5 28.5T600-80H200Zm160-240v-480 480Z "/>
126+ </ svg >
127+ </ button >
128+ </ div >
129+ </ div >
27130 </ section >
131+
132+ < footer >
133+ < a href ="https://github.com/terslanf/glotter "> Glotter</ a > by Ters. Licenced under < a href ="https://www.gnu.org/licenses/agpl-3.0.en.html "> AGPL 3.0</ a >
134+ < br />
135+ < a href ="/source-code "> Click here</ a > to get the source code.
136+ </ footer >
28137
29138 < script >
139+ const nonLatinLangs = [ "ar" , "be" , "bg" , "bn" , "el" , "fa" , "gu" , "he" , "hi" , "ja" , "kn" , "ko" , "ml" , "mt" , "ru" , "sr" , "ta" , "te" , "uk" , "zh" ] ;
140+ const debounce = ( callback , delay ) => {
141+ let timeout ;
142+ return ( ...args ) => {
143+ clearTimeout ( timeout ) ;
144+ timeout = setTimeout ( ( ) => {
145+ callback . apply ( this , args ) ;
146+ } , delay ) ;
147+ } ;
148+ } ;
149+
150+ const transliterateText = async ( text , lang ) => {
151+ if ( ! nonLatinLangs . includes ( lang ) ) {
152+ return '' ;
153+ }
154+
155+ try {
156+ const res = await fetch ( "/transliterate" , {
157+ method : "POST" ,
158+ headers : {
159+ "Content-Type" : "application/json"
160+ } ,
161+ body : JSON . stringify ( {
162+ lang : lang ,
163+ text : text
164+ } )
165+ } ) ;
166+ if ( ! res . ok ) {
167+ throw new Error ( "Error during transliteration" ) ;
168+ }
169+
170+ const data = await res . json ( ) ;
171+ return data . transliteration ;
172+ } catch ( err ) {
173+ window . alert ( "Transliteration failed" ) ;
174+ return null ;
175+ }
176+ } ;
177+
30178 window . addEventListener ( "load" , async ( ) => {
31179 fetch ( "/version" )
32180 . then ( async ( res ) => {
@@ -48,20 +196,132 @@ <h2>Supported Languges</h2>
48196 }
49197
50198 const languages = await res . json ( ) ;
51- const languagesList = document . getElementById ( 'languages' ) ;
52- const listFragment = document . createDocumentFragment ( ) ;
53- languages . forEach ( ( lang ) => {
54- const listItem = document . createElement ( 'li' ) ;
55- listItem . innerText = lang . code + ' - ' + lang . name ;
56- listFragment . appendChild ( listItem ) ;
199+ const sourceLanguagesList = document . getElementById ( 'select-source' ) ;
200+ const targetLanguagesList = document . getElementById ( 'select-target' ) ;
201+ const sourceOptionsFragment = document . createDocumentFragment ( ) ;
202+ const targetOptionsFragment = document . createDocumentFragment ( ) ;
203+ languages . sort ( ( a , b ) => a . name . localeCompare ( b . name ) ) . forEach ( ( lang ) => {
204+ const sourceOptionItem = document . createElement ( 'option' ) ;
205+ sourceOptionItem . value = lang . code ;
206+ sourceOptionItem . innerText = lang . name ;
207+ sourceOptionsFragment . appendChild ( sourceOptionItem ) ;
208+
209+ const targetOptionItem = document . createElement ( 'option' ) ;
210+ targetOptionItem . value = lang . code ;
211+ targetOptionItem . innerText = lang . name ;
212+ targetOptionsFragment . appendChild ( targetOptionItem ) ;
57213 } ) ;
58- languagesList . appendChild ( listFragment ) ;
214+ sourceLanguagesList . appendChild ( sourceOptionsFragment ) ;
215+ targetLanguagesList . appendChild ( targetOptionsFragment ) ;
216+
217+ // Set default selections
218+ sourceLanguagesList . value = 'en' ;
219+ targetLanguagesList . value = 'de' ;
59220 } )
60221 . catch ( ( err ) => {
61222 window . alert ( "Could not fetch supported languages" ) ;
62223 } ) ;
63224 } ) ;
225+
226+ const swapButton = document . getElementById ( 'swap-button' ) ;
227+ swapButton . addEventListener ( "click" , ( ) => {
228+ const sourceSelect = document . getElementById ( 'select-source' ) ;
229+ const targetSelect = document . getElementById ( 'select-target' ) ;
230+ const targetTransliterationDiv = document . getElementById ( 'target-transliteration' ) ;
231+ const tempValue = sourceSelect . value ;
232+ sourceSelect . value = targetSelect . value ;
233+ targetSelect . value = tempValue ;
234+
235+ const sourceTextArea = document . getElementById ( 'source-text' ) ;
236+ const targetTextArea = document . getElementById ( 'target-text' ) ;
237+ sourceTextArea . value = targetTextArea . value ;
238+ targetTextArea . value = "" ;
239+ targetTransliterationDiv . innerText = "" ;
240+
241+ transliterateText ( sourceTextArea . value , sourceSelect . value )
242+ . then ( ( transliteration ) => {
243+ const transliterationDiv = document . getElementById ( 'source-transliteration' ) ;
244+ if ( transliteration ) {
245+ transliterationDiv . innerText = transliteration ;
246+ } else {
247+ transliterationDiv . innerText = '' ;
248+ }
249+ } ) ;
250+ } ) ;
251+
252+ const pasteButton = document . getElementById ( 'paste-button' ) ;
253+ pasteButton . addEventListener ( "click" , async ( ) => {
254+ const sourceTextArea = document . getElementById ( 'source-text' ) ;
255+ try {
256+ const text = await navigator . clipboard . readText ( ) ;
257+ sourceTextArea . value = text ;
258+ } catch ( err ) {
259+ window . alert ( "Could not read from clipboard" ) ;
260+ }
261+ } ) ;
262+
263+ const copyButton = document . getElementById ( 'copy-button' ) ;
264+ copyButton . addEventListener ( "click" , async ( ) => {
265+ const targetTextArea = document . getElementById ( 'target-text' ) ;
266+ try {
267+ await navigator . clipboard . writeText ( targetTextArea . value ) ;
268+ } catch ( err ) {
269+ window . alert ( "Could not write to clipboard" ) ;
270+ }
271+ } ) ;
272+
273+ const translateButton = document . getElementById ( 'translate-button' ) ;
274+ translateButton . addEventListener ( "click" , ( ) => {
275+ const sourceText = document . getElementById ( 'source-text' ) . value ;
276+ const sourceLang = document . getElementById ( 'select-source' ) . value ;
277+ const targetLang = document . getElementById ( 'select-target' ) . value ;
278+ fetch ( "/translate" , {
279+ method : "POST" ,
280+ headers : {
281+ "Content-Type" : "application/json"
282+ } ,
283+ body : JSON . stringify ( {
284+ from : sourceLang ,
285+ to : targetLang ,
286+ text : sourceText
287+ } )
288+ } )
289+ . then ( async ( res ) => {
290+ if ( ! res . ok ) {
291+ throw new Error ( "Error during translation" ) ;
292+ }
293+
294+ const data = await res . json ( ) ;
295+ document . getElementById ( 'target-text' ) . value = data . translation ;
296+
297+ transliterateText ( data . translation , targetLang )
298+ . then ( ( transliteration ) => {
299+ const transliterationDiv = document . getElementById ( 'target-transliteration' ) ;
300+ if ( transliteration ) {
301+ transliterationDiv . innerText = transliteration ;
302+ } else {
303+ transliterationDiv . innerText = '' ;
304+ }
305+ } ) ;
306+ } )
307+ . catch ( ( err ) => {
308+ window . alert ( "Translation failed" ) ;
309+ } ) ;
310+ } ) ;
311+
312+ const sourceTextArea = document . getElementById ( 'source-text' ) ;
313+ sourceTextArea . addEventListener ( "input" , debounce ( ( ) => {
314+ const sourceLang = document . getElementById ( 'select-source' ) . value ;
315+ transliterateText ( sourceTextArea . value , sourceLang )
316+ . then ( ( transliteration ) => {
317+ const transliterationDiv = document . getElementById ( 'source-transliteration' ) ;
318+ if ( transliteration ) {
319+ transliterationDiv . innerText = transliteration ;
320+ } else {
321+ transliterationDiv . innerText = '' ;
322+ }
323+ } ) ;
324+ } , 300 ) ) ;
64325 </ script >
65326 </ body >
66327</ html >
67-
0 commit comments