diff --git a/app/cache.js b/app/cache.js new file mode 100644 index 0000000..8b4e2e2 --- /dev/null +++ b/app/cache.js @@ -0,0 +1,126 @@ + +export /*abstract */class Cache { + /** + * @param {string} key + * @return {boolean} + * @author fenris + */ + /*protected */has(key) { + throw (new Error('not implemented')); + } + + /** + * @param {string} key + * @return {any} + * @author fenris + */ + /*protected */fetch(key) { + throw (new Error('not implemented')); + } + + /** + * @param {string} key + * @param {any} value + * @author fenris + */ + /*protected */store(key, value) { + throw (new Error('not implemented')); + } + + /** + * @param {string} key + * @param {()=>Promise} retrieve + * @return {Promise} + * @author fenris + */ + /*public */async get(key, retrieve) { + if (this.has(key)) { + const value = this.fetch(key); + return Promise.resolve(value); + } else { + const value = await retrieve(); + this.store(key, value); + return Promise.resolve(value); + } + } +} + + +/** + * @author fenris + */ +class CacheNone extends Cache { + /** + * @author fenris + */ + /*public */constructor() { + super(); + } + + /** + * @author fenris + */ + /*protected */has(key) { + return false; + } + + /** + * @author fenris + */ + /*protected */fetch(key) { + throw (new Error('not possible')); + } + + /** + * @author fenris + */ + /*protected */store(key, value) { + } +} + + +/** + * @author fenris + */ +export class CacheLocalstorage extends Cache { + /** + * @param {string} [corner] for separating the cache instance from others + * @author fenris + */ + /*public */constructor(corner = null) { + super(); + this.corner = corner; + } + + /** + * @author fenris + */ + /*private */augmentKey(key) { + return ((this.corner === null) ? key : (this.corner + '/' + key)); + } + + /** + * @author fenris + */ + /*protected */has(key) { + return (window.localStorage.getItem(this.augmentKey(key)) !== null); + } + + /** + * @author fenris + */ + /*protected */fetch(key) { + const valueRaw = window.localStorage.getItem(this.augmentKey(key)); + const value = JSON.parse(valueRaw); + return value; + } + + /** + * @author fenris + */ + /*protected */store(key, value) { + const valueRaw = JSON.stringify(value); + window.localStorage.setItem(this.augmentKey(key), valueRaw); + } +} + diff --git a/app/file.js b/app/file.js new file mode 100644 index 0000000..739a599 --- /dev/null +++ b/app/file.js @@ -0,0 +1,40 @@ + +/** + * @param {string} path + * @return Promise + * @todo use Util.fetch instead? + * @author fenris + */ +export async function read (path) { + return ( + new Promise( + (resolve, reject) => { + let request = new XMLHttpRequest(); + request.open('GET', '/' + path, true); + request.onreadystatechange = () => { + switch (request.readyState) { + case XMLHttpRequest.DONE: { + switch (request.status) { + case 0: { + reject(new Error('XMLHttpRequest failed')); + break; + } + default: { + resolve(request.responseText); + break; + } + } + break; + } + default: { + console.warn('unhandled readyState "' + request.readyState + '"'); + break; + } + } + }; + request.send(null); + } + ) + ); +} + diff --git a/app/index.js b/app/index.js index 15737e9..c8a2377 100644 --- a/app/index.js +++ b/app/index.js @@ -11,6 +11,7 @@ import _dompurify from 'dompurify' import keyboardjs from 'keyboardjs' import { ContinuousVoiceHandler, PushToTalkVoiceHandler, VADVoiceHandler, initVoice } from './voice' +import {initialize as localizationInitialize, translate} from './loc'; const dompurify = _dompurify(window) @@ -1013,18 +1014,28 @@ function userToState () { var voiceHandler var testVoiceHandler -initVoice(data => { - if (testVoiceHandler) { - testVoiceHandler.write(data) - } - if (!ui.client) { - if (voiceHandler) { - voiceHandler.end() +function translateEverything() { +} + +async function main() { + await localizationInitialize(navigator.language); + translateEverything(); + initVoice(data => { + if (testVoiceHandler) { + testVoiceHandler.write(data) } - voiceHandler = null - } else if (voiceHandler) { - voiceHandler.write(data) - } -}, err => { - log('Cannot initialize user media. Microphone will not work:', err) -}) + if (!ui.client) { + if (voiceHandler) { + voiceHandler.end() + } + voiceHandler = null + } else if (voiceHandler) { + voiceHandler.write(data) + } + }, err => { + log('Cannot initialize user media. Microphone will not work:', err) + }) +} + +main(); + diff --git a/app/loc.js b/app/loc.js new file mode 100644 index 0000000..1328dc6 --- /dev/null +++ b/app/loc.js @@ -0,0 +1,121 @@ +import {CacheLocalstorage} from './cache'; +import {read as fileRead} from './file'; +// import {Util} from 'util'; + + +/** + * the relative path to the directory containing the JSON localization files + * + * @var {string} + * @author fenris + */ +var _directory = 'loc'; + + +/** + * the default language to use + * + * @var {string} + * @author fenris + */ +var _languageDefault = null; + + +/** + * the fallback language to use + * + * @var {string} + * @author fenris + */ +var _languageFallback = null; + + +/** + * @var {Cache} + * @author fenris + */ +var _cache = null; + + +/** + * two level map with ISO-639-1 code as first key and translation id as second key + * + * @var {Map>} + * @author fenris + */ +var _data = {}; + + +/** + * @param {string} language + * @return Promise> + * @author fenris + */ +async function retrieveData (language) { + const regexp = (new RegExp("^([a-z]{2})$")); + if (regexp.exec(language) === null) { + return Promise.reject(new Error('invalid language code "' + language + '"')); + } else { + const path = (_directory + '/' + language + '.json'); + let content; + try { + content = await fileRead(path); + } catch (exception) { + return Promise.reject(new Error('could not load localization data for language "' + language + '": ' + error.toString())); + } + let data; + try { + data = JSON.parse(content); + } catch (exception) { + return Promise.reject(new Error('invalid JSON localization data for language "' + language + '": ' + exception.toString())); + } + return Promise.resolve(data); + } +} + + +/** + * @param {string} languageDefault + * @param {string} [languageFallback] + * @author fenris + */ +export async function initialize (languageDefault, languageFallback = 'en') { + _cache = new CacheLocalstorage('loc'); + _languageFallback = languageFallback; + _languageDefault = languageDefault; + for (const language of [_languageFallback, _languageDefault]) { + if (_data.hasOwnProperty(language)) continue; + console.log('--', 'loading localization data for language "' + language + '" ...'); + let data; + try { + data = await _cache.get(language, () => retrieveData(language)); + } catch (exception) { + console.warn(exception.toString()); + } + _data[language] = data; + } +} + + +/** + * gets a translation by its key for a specific language + * + * @param {string} key + * @param {string} [languageChosen] + * @return {string} + * @author fenris + */ +export function translate (key, languageChosen = _languageDefault) { + let result = undefined; + for (const language of [languageChosen, _languageFallback]) { + if (_data.hasOwnProperty(language) && (_data[language] !== undefined) && _data[language].hasOwnProperty(key)) { + result = _data[language][key]; + break; + } + } + if (result === undefined) { + result = ('{{' + key + '}}'); + } + return result; +} + diff --git a/tools/build b/tools/build new file mode 100755 index 0000000..0430b43 --- /dev/null +++ b/tools/build @@ -0,0 +1,8 @@ +#!/usr/bin/env sh + +npm run build + +## loc +mkdir -p dist/loc +cp -ruv loc/* dist/loc/ +