diff options
author | 2023-07-30 19:54:00 +0530 | |
---|---|---|
committer | 2023-07-30 19:54:00 +0530 | |
commit | 5b0ecbfc06ef328dd2d31ff4d48f11622fc31be4 (patch) | |
tree | 3e7a13dfe0514f79b18dd219417f486e9893ada3 | |
download | translator-main.tar translator-main.tar.gz translator-main.tar.bz2 translator-main.tar.lz translator-main.tar.xz translator-main.tar.zst translator-main.zip |
Initial Commitmain
Signed-off-by: Marc Pervaz Boocha <mboocha@sudomsg.xyz>
-rw-r--r-- | README.md | 17 | ||||
-rw-r--r-- | Translate.js | 136 | ||||
-rw-r--r-- | create.js | 142 | ||||
-rw-r--r-- | index.css | 53 | ||||
-rw-r--r-- | index.html | 12 | ||||
-rw-r--r-- | index.js | 32 |
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) |