aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
author2023-07-30 19:54:00 +0530
committer2023-07-30 19:54:00 +0530
commit5b0ecbfc06ef328dd2d31ff4d48f11622fc31be4 (patch)
tree3e7a13dfe0514f79b18dd219417f486e9893ada3
downloadtranslator-5b0ecbfc06ef328dd2d31ff4d48f11622fc31be4.tar
translator-5b0ecbfc06ef328dd2d31ff4d48f11622fc31be4.tar.gz
translator-5b0ecbfc06ef328dd2d31ff4d48f11622fc31be4.tar.bz2
translator-5b0ecbfc06ef328dd2d31ff4d48f11622fc31be4.tar.lz
translator-5b0ecbfc06ef328dd2d31ff4d48f11622fc31be4.tar.xz
translator-5b0ecbfc06ef328dd2d31ff4d48f11622fc31be4.tar.zst
translator-5b0ecbfc06ef328dd2d31ff4d48f11622fc31be4.zip
Initial Commitmain
Signed-off-by: Marc Pervaz Boocha <mboocha@sudomsg.xyz>
-rw-r--r--README.md17
-rw-r--r--Translate.js136
-rw-r--r--create.js142
-rw-r--r--index.css53
-rw-r--r--index.html12
-rw-r--r--index.js32
6 files changed, 392 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2df2182
--- /dev/null
+++ b/README.md
@@ -0,0 +1,17 @@
+# Translate
+
+This app translate the text between languages live. It is based on the libretranslate api
+
+## Installation
+
+This is a static website. Copy all the files to the webroot of your webserver.
+
+Note: This requires a [libretranslate](https://github.com/LibreTranslate/LibreTranslate) server. Change to you endpoint in line 5 of index.js
+
+## Execution
+
+To debug this, you can setup a simple localhost server for development and testing.
+
+```bash
+python -m http.server
+```
diff --git a/Translate.js b/Translate.js
new file mode 100644
index 0000000..2a89208
--- /dev/null
+++ b/Translate.js
@@ -0,0 +1,136 @@
+// @ts-check
+/**
+ * Translate Api wrapper
+ */
+export default class Translate {
+ /**
+ * @type {URL}
+ */
+ baseURL
+ /**
+ * @type {string?}
+ */
+ apikeys
+ cachedResponses = new Map()
+ /**
+ * @param {string | URL} baseURL The url of the api server
+ * @param {string?} [apiKeys] The api keys of the endpoint
+ */
+ constructor(baseURL = "https://libretranslate.com", apiKeys) {
+ this.baseURL = new URL(baseURL)
+ this.apiKeys = apiKeys
+ }
+
+ /**
+ * Call the POST method for the endpoint
+ * @param {string} endpoint The endpoint on the server to call
+ * @param {Object} params The parameters of the api to be sent to the server
+ * @returns {Promise<any>} The object respose sent from the server
+ */
+ async call(endpoint, params = {}) {
+ const url = new URL(endpoint, this.baseURL)
+
+ const res = await fetch(url, {
+ method: "POST",
+ body: JSON.stringify({ ...params, api_key: this.apiKeys }),
+ headers: { "Content-Type": "application/json" }
+ })
+
+ const data = await res.json()
+ if (!res.ok) {
+ throw new Error(data.error || 'Api Error')
+ }
+
+ return data
+ }
+
+ /**
+ * Call the GET method for the endpoint and cache them
+ * @param {string} endpoint The endpoint on the server to call
+ * @returns {Promise<any>} The object respose sent from the server
+ */
+ async callCache(endpoint) {
+ const url = new URL(endpoint, this.baseURL)
+
+ if (this.cachedResponses.has(url)) {
+ return this.cachedResponses.get(url)
+ }
+
+ const res = await fetch(url, {
+ headers: { "Content-Type": "application/json" }
+ })
+
+ const data = await res.json()
+ if (!res.ok) {
+ throw new Error(data?.error ?? 'Api Error')
+ }
+
+ this.cachedResponses.set(url, data)
+
+ return data
+ }
+
+ /**
+ * @typedef {Object} DetectLang
+ * @property {Number} confidence
+ * @property {string} language
+ */
+ /**
+ * Detect the language of the Text
+ * @param {string} text
+ * @returns {Promise<DetectLang[]>} list of all language
+ */
+ async detect(text = "") {
+ if (text === "") {
+ return [{ language: 'en', confidence: 0 }]
+ }
+ return this.call('detect', { q: text })
+ }
+
+ /**
+ * @typedef {Object} LangList
+ * @property {string} code
+ * @property {string} name
+ * @property {string[]} targets
+ */
+ /**
+ * Gets the list of Languages
+ * @returns {Promise<LangList[]>} List of Languages
+ */
+ async languages() {
+ return this.callCache('languages')
+ }
+
+ /**
+ * @typedef {Object} TranslatorSettings
+ * @property {boolean} apiKeys
+ * @property {Number} charLimit
+ * @property {Number} frontendTimeout
+ * @property {boolean} keyRequired
+ * @property {Object} language
+ * @property {boolean} suggestions
+ * @property {string[]} supportedsupportedFilesFormat
+ */
+ /**
+ * Get the settings for the frontend
+ * @returns {Promise<TranslatorSettings>} The list of settings
+ */
+ async settings() {
+ return this.callCache('/frontend/settings')
+ }
+
+ /**
+ * Transtale the Text
+ * @param {string} text Input Text
+ * @param {string} source Source Language
+ * @param {string} target Target Language
+ * @returns {Promise<string>} Translated Text
+ */
+ async translate(text = "", source, target) {
+ if (text === "") {
+ return ""
+ }
+ const res = await this.call('translate', { q: text, source, target })
+ return res.translatedText
+ }
+}
diff --git a/create.js b/create.js
new file mode 100644
index 0000000..a5b1036
--- /dev/null
+++ b/create.js
@@ -0,0 +1,142 @@
+//@ts-check
+
+import Translate from './Translate.js'
+import { debounce } from './index.js'
+
+/**
+ * Create The menubar
+ * @returns {HTMLMenuElement}
+ */
+export function createMenu() {
+ const header = document.createElement('header')
+ document.body.append(header)
+
+ const menu = document.createElement('menu')
+ header.append(menu)
+ menu.role = 'menubar'
+ return menu
+}
+/**
+ * @param {HTMLMenuElement} menu
+ * @param {Translate} translate
+ * @returns {Promise<{source: HTMLSelectElement, target: HTMLSelectElement}>}
+ */
+export async function createDropDown(menu, translate) {
+ const source = createSourceElement(menu)
+ const target = createTargetElement(menu)
+
+ const langlist = await translate.languages()
+
+ langlist.forEach((element) => {
+ addSelectionOpt(source, element.code, element.name)
+ addSelectionOpt(target, element.code, element.name)
+ })
+
+ const { language } = await translate.settings()
+ source.value = language.source.code
+ target.value = language.target.code
+
+ const setup = async () => {
+ const langList = await translate.languages()
+ const src = source.value
+ const targetList = langList.find(obj => obj.code === src)?.targets ?? []
+ Array.from(target.options).forEach(element => {
+ element.disabled = !targetList.includes(element.value)
+ })
+ }
+
+ source.addEventListener("change", setup)
+ setup()
+ return { source, target }
+}
+/**
+ * @param {HTMLSelectElement} source
+ * @param {string} value
+ * @param {string} text
+ */
+function addSelectionOpt(source, value, text = value) {
+ const opt = document.createElement("option")
+ opt.textContent = text
+ opt.value = value
+ source.add(opt)
+}
+
+/**
+ * Create the translator boxes
+ * @param {Translate} translate
+ * @param {HTMLSelectElement} sourceSelect
+ * @param {HTMLSelectElement} targetSelect
+ */
+export async function createEditorWidget(translate, sourceSelect, targetSelect) {
+ const input = document.createElement('pre')
+ document.body.appendChild(input)
+ input.id = 'editor'
+ input.contentEditable = "true"
+
+ const output = document.createElement('pre')
+ document.body.appendChild(output)
+ output.id = 'output'
+
+ const translateHandler = debounce(async () => {
+ output.textContent = await translate.translate(
+ input.textContent ?? "",
+ sourceSelect.value,
+ targetSelect.value
+ )
+ }, (await translate.settings()).frontendTimeout)
+
+ sourceSelect.addEventListener("change", translateHandler)
+ targetSelect.addEventListener("change", translateHandler)
+ input.addEventListener("input", translateHandler)
+
+ const sourceLang = () => input.lang = sourceSelect.value
+ sourceLang()
+ sourceSelect.addEventListener("change", sourceLang)
+
+ const targetLang = () => output.lang = targetSelect.value
+ targetLang()
+ sourceSelect.addEventListener("change", targetLang)
+}
+/**
+ * @param {HTMLMenuElement} menu
+ * @returns {HTMLSelectElement}
+ */
+function createTargetElement(menu) {
+ const menuItem = createMenuItem(menu)
+ const { label, select } = createLabeledSelect('target', 'Translate Into: ')
+ menuItem.append(label, select)
+ return select
+}
+/**
+ * @param {HTMLMenuElement} menu
+ */
+function createSourceElement(menu) {
+ const menuItem = createMenuItem(menu)
+ const { label, select } = createLabeledSelect('source', 'TranslateFrom: ')
+ menuItem.append(label, select)
+ addSelectionOpt(select, "auto", "Auto Detectiom")
+ return select
+}
+/**
+ * @param {string} id
+ * @param {string} label
+ * @returns {{label: HTMLLabelElement, select: HTMLSelectElement}}
+ */
+function createLabeledSelect(id, label) {
+ const labelElement = document.createElement('label')
+ labelElement.htmlFor = id
+ labelElement.textContent = label
+ const select = document.createElement('select')
+ select.id = id
+ return { label: labelElement, select }
+}
+/**
+ * @param {HTMLMenuElement} menu The menubar
+ * @returns {HTMLLIElement}
+ */
+function createMenuItem(menu) {
+ const menuItem = document.createElement('li')
+ menu.append(menuItem)
+ menuItem.role = 'menuitem'
+ return menuItem
+}
diff --git a/index.css b/index.css
new file mode 100644
index 0000000..3ec2051
--- /dev/null
+++ b/index.css
@@ -0,0 +1,53 @@
+*{
+ box-sizing: inherit;
+}
+
+html {
+ box-sizing: border-box;
+}
+
+menu > li {
+ display: inline;
+ list-style: none;
+ margin: 0.5rem;
+}
+
+@media screen{
+ html {
+ color-scheme: light dark;
+ height: 90vh;
+ }
+
+ body {
+ display: grid;
+ grid-template-areas:
+ "head head"
+ "main output";
+ grid-template-rows: fit-content(1rem) auto;
+ grid-template-columns: 1fr 1fr;
+ gap: 1rem;
+ height: 100%;
+ }
+
+ header {
+ grid-area: head;
+ background: rgb(117, 117, 117);
+ color-scheme: light;
+ color: white;
+ }
+
+ pre {
+ border: 1px solid;
+ height: 100%;
+ padding: 1rem;
+ overflow: scroll;
+ }
+
+ #editor {
+ grid-area: main;
+ }
+
+ #output {
+ grid-area: output;
+ }
+}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..2d551a4
--- /dev/null
+++ b/index.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>Translator</title>
+ <link rel="stylesheet" href="index.css">
+ <script type=module src="index.js"></script>
+</head>
+
+</html> \ No newline at end of file
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..ccfe99e
--- /dev/null
+++ b/index.js
@@ -0,0 +1,32 @@
+// @ts-check
+import Translate from './Translate.js'
+import { createMenu, createDropDown, createEditorWidget } from './create.js'
+
+const translate = new Translate("http://localhost:5000")
+
+/**
+ * Debounce
+ * @param {(...args: any[]) => any} func
+ * @param {Number} timeout
+ * @returns {(...args:any[]) => void}
+ */
+
+export function debounce(func, timeout) {
+ var timer
+ return function (...args) {
+ clearTimeout(timer)
+ timer = setTimeout(() => {
+ func.apply(this, args)
+ }, timeout)
+ }
+}
+ /**
+ * The Main Function
+ */
+async function main() {
+ const menu = createMenu()
+ const { source, target } = await createDropDown(menu, translate)
+ await createEditorWidget(translate, source, target)
+}
+
+window.addEventListener("load", main)