1
Fork 0
mirror of https://github.com/jech/galene.git synced 2024-11-22 16:45:58 +01:00

feat: i18n

This commit is contained in:
Lucian Petic 2021-03-06 19:39:20 +01:00
parent 3bf0f9ef4c
commit fe7c8cad8f
6 changed files with 319 additions and 73 deletions

View file

@ -19,7 +19,7 @@
<div class="row full-height">
<nav id="left-sidebar">
<div class="users-header">
<div class="galene-header">Galène</div>
<div class="galene-header" data-i18n="app"></div>
</div>
<div class="header-sep"></div>
<div id="users"></div>
@ -28,7 +28,7 @@
<header>
<nav class="topnav navbar navbar-expand navbar-light fixed-top">
<div id="header">
<div class="collapse" title="Collapse left panel" id="sidebarCollapse">
<div class="collapse" data-i18n-title="collapse-left-panel" id="sidebarCollapse">
<svg class="svg-inline--fa" aria-hidden="true" data-icon="align-left" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path fill="currentColor" d="M288 44v40c0 8.837-7.163 16-16 16H16c-8.837 0-16-7.163-16-16V44c0-8.837 7.163-16 16-16h256c8.837 0 16 7.163 16 16zM0 172v40c0 8.837 7.163 16 16 16h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16zm16 312h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm256-200H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16h256c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16z"></path>
</svg>
@ -39,36 +39,36 @@
<ul class="nav-menu">
<li>
<button id="presentbutton" class="invisible btn btn-success">
<i class="fas fa-play" aria-hidden="true"></i><span class="nav-text"> Ready</span>
<i class="fas fa-play" aria-hidden="true"></i><span class="nav-text" data-i18n="ready">&nbsp;</span>
</button>
</li>
<li>
<button id="unpresentbutton" class="invisible btn btn-cancel">
<i class="fas fa-stop" aria-hidden="true"></i><span class="nav-text"> Panic</span>
<i class="fas fa-stop" aria-hidden="true"></i><span class="nav-text" data-i18n="panic">&nbsp;</span>
</button>
</li>
<li>
<div id="mutebutton" class="nav-link nav-button">
<span><i class="fas fa-microphone-slash" aria-hidden="true"></i></span>
<label>Mute</label>
<label data-i18n="mute"></label>
</div>
</li>
<li>
<div id="sharebutton" class="invisible nav-link nav-button">
<span><i class="fas fa-share-square" aria-hidden="true"></i></span>
<label>Share Screen</label>
<label data-i18n="share-screen"></label>
</div>
</li>
<li>
<div id="unsharebutton" class="invisible nav-link nav-button nav-cancel">
<span><i class="fas fa-window-close" aria-hidden="true"></i></span>
<label>Unshare Screen</label>
<label data-i18n="unshare-screen"></label>
</div>
</li>
<li>
<div id="stopvideobutton" class="invisible nav-link nav-button nav-cancel">
<span><i class="fas fa-window-close" aria-hidden="true"></i></span>
<label>Stop Video</label>
<label data-i18n="stop-video"></label>
</div>
</li>
<li>
@ -83,7 +83,7 @@
<div class="coln-left" id="left">
<div id="chat">
<div id="chatbox">
<div class="close-chat" id="close-chat" title="Hide chat">
<div class="close-chat" id="close-chat" data-i18n-title="hide-chat">
<span class="close-icon"></span>
</div>
<div id="box"></div>
@ -100,7 +100,7 @@
<div class="coln-right" id="right">
<span class="show-video blink" id="switch-video"><i class="fas fa-exchange" aria-hidden="true"></i></span>
<div class="collapse-video" id="collapse-video">
<i class="far fa-comment-alt open-chat" title="Open chat"></i>
<i class="far fa-comment-alt open-chat" data-i18n-title="open-chat"></i>
</div>
<div class="video-container no-video" id="video-container">
<div id="expand-video" class="expand-video">
@ -110,30 +110,30 @@
<div class="login-container invisible" id="login-container">
<div class="login-box">
<form id="userform" class="userform">
<label for="username">Username</label>
<label for="username" data-i18n="username"></label>
<input id="username" type="text" name="username"
autocomplete="username" class="form-control"/>
<label for="password">Password</label>
<label for="password" data-i18n="password"></label>
<input id="password" type="password" name="password"
autocomplete="current-password" class="form-control"/>
<label>Auto ready</label>
<label data-i18n="auto-ready"></label>
<div class="present-switch">
<p class="switch-radio">
<input id="presentoff" type="radio" name="presentradio" value="" checked/>
<label for="presentoff">Disabled</label>
<label for="presentoff" data-i18n="disabled"></label>
</p>
<p class="switch-radio">
<input id="presentmike" type="radio" name="presentradio" value="mike"/>
<label for="presentmike">Enable microphone</label>
<label for="presentmike" data-i18n="enable-micro"></label>
</p>
<p class="switch-radio">
<input id="presentboth" type="radio" name="presentradio" value="both"/>
<label for="presentboth">Enable camera and microphone</label>
<label for="presentboth" data-i18n="enable-cam-micro"></label>
</p>
</div>
<div class="clear"></div>
<div class="connect">
<input id="connectbutton" type="submit" class="btn btn-blue" value="Connect"/>
<input id="connectbutton" type="submit" class="btn btn-blue" data-i18n-value="connect"/>
</div>
</form>
<div class="clear"></div>
@ -147,7 +147,7 @@
<div id="sidebarnav" class="sidenav">
<div class="sidenav-header">
<h2>Settings</h2>
<h2 data-i18n="settings"></h2>
<a class="closebtn" id="clodeside"><i class="fas fa-times" aria-hidden="true"></i></a>
</div>
<div class="sidenav-content" id="optionsdiv">
@ -163,76 +163,84 @@
<div class="user-logout">
<a id="disconnectbutton">
<span class="logout-icon"><i class="fas fa-sign-out-alt"></i></span>
<span class="logout-text">Logout</span>
<span class="logout-text" data-i18n="logout"></span>
</a>
</div>
</div>
</div>
<div id="mediaoptions" class="invisible">
<fieldset>
<legend>Media Options</legend>
<label for="videoselect" class="sidenav-label-first">Camera:</label>
<legend data-i18n="media-options"></legend>
<label for="videoselect" class="sidenav-label-first" data-i18n="camera"></label>
<select id="videoselect" class="select select-inline">
<option value="">off</option>
</select>
<label for="audioselect" class="sidenav-label">Microphone:</label>
<label for="audioselect" class="sidenav-label" data-i18n="micro"></label>
<select id="audioselect" class="select select-inline">
<option value="">off</option>
<option value="" data-i18n="off"></option>
</select>
<form>
<input id="mirrorbox" type="checkbox" checked/>
<label for="mirrorbox">Mirror view</label>
<label for="mirrorbox" data-i18n="mirror-view"></label>
</form>
<form>
<input id="blackboardbox" type="checkbox"/>
<label for="blackboardbox">Blackboard mode</label>
<label for="blackboardbox" data-i18n="blackboard-mode"></label>
</form>
</fieldset>
</div>
<fieldset>
<legend>Other Settings</legend>
<legend data-i18n="other-settings"></legend>
<form id="filterform">
<label for="filterselect" class="sidenav-label-first">Filter:</label>
<label for="filterselect" class="sidenav-label-first" data-i18n="filter"></label>
<select id="filterselect" class="select select-inline">
<option value="" selected>none</option>
<option value="" selected data-i18n="none"></option>
</select>
</form>
<form id="sendform">
<label for="sendselect" class="sidenav-label-first">Send:</label>
<label for="sendselect" class="sidenav-label-first" data-i18n="send"></label>
<select id="sendselect" class="select select-inline">
<option value="lowest">lowest</option>
<option value="low">low</option>
<option value="normal" selected>normal</option>
<option value="unlimited">unlimited</option>
<option value="lowest" data-i18n="lowest"></option>
<option value="low" data-i18n="low"></option>
<option value="normal" selected data-i18n="normal"></option>
<option value="unlimited" data-i18n="unlimited"></option>
</select>
</form>
<form id="languageform">
<label for="languageselect" class="sidenav-label" data-i18n="language"></label>
<select id="languageselect" class="select select-inline">
<option value="fr" data-i18n="fr">FR</option>
<option value="en" data-i18n="en">EN</option>
</select>
</form>
<form id="requestform">
<label for="requestselect" class="sidenav-label">Receive:</label>
<label for="requestselect" class="sidenav-label" data-i18n="receive"></label>
<select id="requestselect" class="select select-inline">
<option value="">nothing</option>
<option value="audio">audio only</option>
<option value="screenshare">screen share</option>
<option value="everything" selected>everything</option>
<option value="" data-i18n="nothing"></option>
<option value="audio" data-i18n="audio-only"></option>
<option value="screenshare" data-i18n="screen-share"></option>
<option value="everything" selected data-i18n="everything"></option>
</select>
</form>
<form>
<input id="activitybox" type="checkbox"/>
<label for="activitybox">Activity detection</label>
<label for="activitybox" data-i18n="activity-detection"></label>
</form>
</fieldset>
<form id="fileform">
<label for="fileinput" class=".sidenav-label-first">Play local file:</label>
<label for="fileinput" class=".sidenav-label-first" data-i18n="play-local-file"></label>
<input type="file" id="fileinput" accept="audio/*,video/*" multiple/>
</form>
</div>
@ -241,19 +249,19 @@
<div id="videocontrols-template" class="invisible">
<div class="video-controls vc-overlay">
<div class="controls-button controls-left">
<span class="video-play" title="Play video">
<span class="video-play" data-i18n-title="play-video">
<i class="fas fa-play"></i>
</span>
<span class="volume" title="Volume">
<span class="volume" data-i18n-title="volume">
<i class="fas fa-volume-up volume-mute" aria-hidden="true"></i>
<input class="volume-slider" type="range" max="100" value="100" min="0" step="5" >
</span>
</div>
<div class="controls-button controls-right">
<span class="pip" title="Picture In Picture">
<span class="pip" data-i18n-title="picture-in-picture">
<i class="far fa-clone" aria-hidden="true"></i>
</span>
<span class="fullscreen" title="Fullscreen">
<span class="fullscreen" data-i18n-title="Fullscreen">
<i class="fas fa-expand" aria-hidden="true"></i>
</span>
</div>
@ -262,6 +270,7 @@
<script src="/protocol.js" defer></script>
<script src="/scripts/toastify.js" defer></script>
<script src="/translations/translation.js" defer></script>
<script src="/galene.js" defer></script>
</body>
</html>

View file

@ -39,6 +39,20 @@ let serverConnection;
*/
let fallbackUserPass = null;
/** @type {Translation} */
let trans = new Translation();
/**
* Change language
*/
async function changeLanguage(){
const element = document.getElementById('languageselect');
await element.addEventListener('change', async function() {
const language = element.options[element.selectedIndex].value;
trans.selectLanguage(language);
await trans.analyse();
})
}
/**
* @param {string} username
@ -308,7 +322,7 @@ function setConnected(connected) {
fillLogin();
userbox.classList.add('invisible');
connectionbox.classList.remove('invisible');
displayError('Disconnected', 'error');
displayError(trans.get('disconnected'), 'error');
hideVideo();
closeVideoControls();
}
@ -344,7 +358,7 @@ function gotDownStream(c) {
};
c.onerror = function(e) {
console.error(e);
displayError(e);
displayError(e.toString());
};
c.ondowntrack = function(track, transceiver, label, stream) {
setMedia(c, false);
@ -602,7 +616,7 @@ getInputElement('fileinput').onchange = function(e) {
for(let i = 0; i < files.length; i++) {
addFileMedia(files[i]).catch(e => {
console.error(e);
displayError(e);
displayError(e.toString());
});
}
input.value = '';
@ -795,7 +809,7 @@ function newUpStream(localId) {
};
c.onerror = function(e) {
console.error(e);
displayError(e);
displayError(e.toString());
};
c.onnegotiationcompleted = function() {
setMaxVideoThroughput(c, getMaxVideoThroughput());
@ -1037,7 +1051,7 @@ async function addLocalMedia(localId) {
if(settings.filter) {
filter = filters[settings.filter];
if(!filter) {
displayWarning(`Unknown filter ${settings.filter}`);
displayWarning(trans.get('unknown-filter') + settings.filter);
}
}
@ -1190,7 +1204,7 @@ async function addFileMedia(file) {
/** @ts-ignore */
stream = video.mozCaptureStream();
else {
displayError("This browser doesn't support file playback");
displayError(trans.get('playback-unsupported'));
return;
}
@ -1216,7 +1230,7 @@ async function addFileMedia(file) {
let muted = getSettings().localMute;
if(presenting && !muted) {
setLocalMute(true, true);
displayWarning('You have been muted');
displayWarning(trans.get('you-muted'));
}
}
c.pc.addTrack(t, stream);
@ -1517,7 +1531,7 @@ function registerControlHandlers(media, container) {
/** @ts-ignore */
media.requestPictureInPicture();
} else {
displayWarning('Picture in Picture not supported.');
displayWarning(trans.get('pic-in-pic-unsupported'));
}
};
} else {
@ -1539,7 +1553,7 @@ function registerControlHandlers(media, container) {
/** @ts-ignore */
media.webkitRequestFullscreen();
} else {
displayWarning('Full screen not supported!');
displayWarning(trans.get('fullscreen-unsupported'));
}
};
} else {
@ -1776,7 +1790,7 @@ async function gotJoined(kind, group, perms, message) {
switch(kind) {
case 'fail':
displayError('The server said: ' + message);
displayError(trans.get('server-say') + message);
this.close();
setButtonsVisibility();
return;
@ -1796,18 +1810,18 @@ async function gotJoined(kind, group, perms, message) {
return;
break;
default:
displayError('Unknown join message');
displayError(trans.get('unknown-join-message'));
this.close();
return;
}
let input = /** @type{HTMLTextAreaElement} */
(document.getElementById('input'));
input.placeholder = 'Type /help for help';
input.placeholder = trans.get('type-help');
setTimeout(() => {input.placeholder = '';}, 8000);
if(typeof RTCPeerConnection === 'undefined')
displayWarning("This browser doesn't support WebRTC");
displayWarning(trans.get('webrtc-unsupported'));
else
this.request(getSettings().request);
@ -1827,9 +1841,7 @@ async function gotJoined(kind, group, perms, message) {
button.disabled = false;
}
} else {
displayMessage(
"Press Ready to enable your camera or microphone"
);
displayMessage(trans.get('press-ready'));
}
}
}
@ -1850,7 +1862,7 @@ function gotUserMessage(id, dest, username, time, privileged, kind, message) {
case 'info':
let from = id ? (username || 'Anonymous') : 'The Server';
if(privileged)
displayError(`${from} said: ${message}`, kind);
displayError(`${from} ${trans.get('said')} ${message}`, kind);
else
console.error(`Got unprivileged message of kind ${kind}`);
break;
@ -1858,7 +1870,7 @@ function gotUserMessage(id, dest, username, time, privileged, kind, message) {
if(privileged) {
setLocalMute(true, true);
let by = username ? ' by ' + username : '';
displayWarning(`You have been muted${by}`);
displayWarning(trans.get('you-muted') + by);
} else {
console.error(`Got unprivileged message of kind ${kind}`);
}
@ -2460,7 +2472,7 @@ function handleInput() {
} else {
let c = commands[cmd];
if(!c) {
displayError(`Uknown command /${cmd}, type /help for help`);
displayError( trans.get('unknown-command')+ cmd + trans.get('type-help'));
return;
}
if(c.predicate) {
@ -2484,7 +2496,7 @@ function handleInput() {
}
if(!serverConnection || !serverConnection.socket) {
displayError("Not connected.");
displayError(trans.get("not-connected"));
return;
}
@ -2547,7 +2559,7 @@ function chatResizer(e) {
document.getElementById('resizer').addEventListener('mousedown', chatResizer, false);
/**
* @param {unknown} message
* @param {string} message
* @param {string} [level]
*/
function displayError(message, level) {
@ -2582,14 +2594,14 @@ function displayError(message, level) {
}
/**
* @param {unknown} message
* @param {string} message
*/
function displayWarning(message) {
return displayError(message, "warning");
}
/**
* @param {unknown} message
* @param {string} message
*/
function displayMessage(message) {
return displayError(message, "info");
@ -2708,11 +2720,14 @@ async function serverConnect() {
await serverConnection.connect(url);
} catch(e) {
console.error(e);
displayError(e.message ? e.message : "Couldn't connect to " + url);
displayError(e.message ? e.message : trans.get('can-t-connect') + url);
}
}
function start() {
async function start() {
await trans.analyse();
await changeLanguage();
group = decodeURIComponent(location.pathname.replace(/^\/[a-z]*\//, ''));
let title = group.charAt(0).toUpperCase() + group.slice(1);
if(group !== '') {

View file

@ -0,0 +1,64 @@
{
"app": "Galène",
"ready": " Ready",
"panic": "Panic",
"mute": "Mute",
"share-screen": "Share Screen",
"unshare Screen": "Unshare Screen",
"stop-video": "Stop Video",
"username": "Username",
"password": "Password",
"connect": "Connect",
"auto-ready": "Auto ready",
"disabled": "Disabled",
"enable-micro": "Enable microphone",
"enable-cam-micro": "Enable camera and microphone",
"settings": "Settings",
"logout": "Logout",
"media-options": "Media Options",
"camera": "Camera:",
"micro": "Microphone:",
"off": "off",
"mirror-view": "Mirror view",
"blackboard-mode": "Blackboard mode",
"other-settings": "Other Settings",
"language": "Language:",
"filter": "Filter:",
"none": "none",
"send": "Send:",
"lowest": "lowest",
"low": "low",
"normal": "normal",
"unlimited": "unlimited",
"receive:": "Receive:",
"nothing": "nothing",
"audio-only": "audio only",
"screen-share1": "screen share",
"everything": "everything",
"activity-detection": "Activity detection",
"play-local-file": "Play local file:",
"collapse-left-panel": "Collapse left panel",
"hide-chat": "Hide chat",
"open-chat": "Open chat",
"play-video": "Play video",
"volume": "Volume",
"pic-in-pic": "Picture in Picture",
"fullscreen": "Fullscreen",
"not-connected": "Not connected.",
"disconnected": "Disconnected",
"can-t-connect": "Couldn't connect to ",
"unknown-filter": "Unknown filter ",
"playback-unsupported": "This browser doesn't support file playback",
"you-muted": "You have been muted",
"pic-in-pic-unsupported": "Picture in Picture not supported.",
"fullscreen-unsupported": "Full screen not supported!",
"serve-say": "The server said: ",
"unknown-join-message": "Unknown join message",
"webrtc-unsupported": "This browser doesn't support WebRTC",
"type-help": "Type /help for help",
"press-ready": "Press Ready to enable your camera or microphone",
"said": "said: ",
"unknown-command ": "Unknown command "
}

View file

@ -0,0 +1,65 @@
{
"app": "Galène",
"ready": " Prêt",
"panic": "Panic",
"mute": "Muet",
"share-screen": "Partage d'écran",
"unshare Screen": "Arrêter le partage d'écran",
"stop-video": "Arrêter la video",
"username": "Pseudo",
"password": "Mot de passe",
"connect": "Connexion",
"auto-ready": "Préparation automatique",
"disabled": "Désactiver",
"enable-micro": "Activer le microphone",
"enable-cam-micro": "Activer la caméra et microphone",
"settings": "Paramètres",
"logout": "Déconnexion",
"media-options": "Options Media",
"camera": "Caméra:",
"micro": "Microphone:",
"off": "off",
"mirror-view": "Vue en miroir",
"blackboard-mode": "Mode tableau noir",
"other-settings": "Autres paramètres",
"language": "Langue:",
"filter": "Filtre:",
"none": "aucun",
"send": "Envoyer:",
"lowest": "le plus bas",
"low": "faible",
"normal": "normal",
"unlimited": "illimité",
"receive:": "Recevoir:",
"nothing": "rien",
"audio-only": "audio uniquement",
"screen-share1": "partage d'écran",
"everything": "tout",
"activity-detection": "Détection d'activité",
"play-local-file": "Lecture du fichier local :",
"collapse-left-panel": "Effondrement du panneau de gauche",
"hide-chat": "Cacher le chat",
"open-chat": "Chat ouvert",
"play-video": "Lire la vidéo",
"volume": "Volume",
"pic-in-pic": "Image dans l'image",
"fullscreen": "Plein écran",
"not-connected": "Pas connecté.",
"disconnected": "Déconnecté",
"can-t-connect": "Pas possible de se connecter à ",
"unknown-filter": "Filtre inconnu ",
"playback-unsupported": "Ce navigateur ne prend pas en charge la lecture",
"you-muted": "Vous avez été mis en sourdine",
"pic-in-pic-unsupported": "L'image dans l'image n'est pas prise en charge.",
"fullscreen-unsupported": "Le plein écran n'est pas pris en charge !",
"serve-say": "Le serveur a dit: ",
"unknown-join-message": "Message d'adhésion inconnu",
"webrtc-unsupported": "Ce navigateur n'est pas compatible avec WebRTC",
"type-help": "Écrire /help pour l'aide",
"press-ready": "Appuyez sur Prêt pour activer votre caméra ou votre microphone",
"said": "a dit: ",
"unknown-command ": "Commande inconnue "
}

View file

@ -0,0 +1,92 @@
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
/**
*
* @constructor
*/
function Translation() {
/**
* Default language
* @type {string}
*/
this.default = 'en';
/**
* Used language
* @type {string|string}
*/
this.language = this.default;
/**
* Available languages
* @type {string[]}
*/
this.languages = ['en', 'fr'];
/**
* Json witch contains translations
* @type {undefined}
*/
this.json = undefined;
this.selectLanguage(navigator.language);
}
/**
*
* @returns {Promise<any>}
*/
Translation.prototype.load = async function (){
const url = window.location.origin + '/translations/i18n.' + this.language + '.json';
return await (await fetch(url)).json();
}
/**
* Analyse all HTML to remplace innerHTML with value linked to the key
* @returns {Promise<void>}
*/
Translation.prototype.analyse = async function (){
this.json = await trans.load();
document.querySelectorAll('[data-i18n]').forEach(item => {
item.innerHTML = this.get(item.getAttribute('data-i18n'))
});
document.querySelectorAll('[data-i18n-title]').forEach(item => {
item.title = this.get(item.getAttribute('data-i18n-title'))
});
document.querySelectorAll('[data-i18n-value]').forEach(item => {
item.value = this.get(item.getAttribute('data-i18n-value'))
});
}
/**
*
* @param key
* @returns string
*/
Translation.prototype.get = function(key) {
return key in this.json ? this.json[key] : key;
};
/**
* Select an available language
* @param language
*/
Translation.prototype.selectLanguage = function (language){
this.language = this.languages.filter(l => l === language).length === 1 ? language : this.default;
}

View file

@ -14,6 +14,7 @@
},
"files": [
"protocol.js",
"galene.js"
"galene.js",
"translations/translation.js"
]
}