Merge branch 'localization' (see #67, based on #78)

This commit is contained in:
Jonas Herzig 2020-05-03 19:00:05 +02:00
commit f6b8bdcb3b
7 changed files with 402 additions and 70 deletions

View file

@ -27,29 +27,29 @@
css: { minimal: minimalView }">
<!-- ko with: connectDialog -->
<div class="connect-dialog dialog" data-bind="visible: visible() && !joinOnly()">
<div class="dialog-header">
<div id="connect-dialog_title" class="dialog-header">
Connect to Server
</div>
<form data-bind="submit: connect">
<table>
<tr data-bind="if: $root.config.connectDialog.address">
<td>Address</td>
<td id="connect-dialog_input_address">Address</td>
<td><input id="address" type="text" data-bind="value: address" required></td>
</tr>
<tr data-bind="if: $root.config.connectDialog.port">
<td>Port</td>
<td id="connect-dialog_input_port">Port</td>
<td><input id="port" type="text" data-bind="value: port" required></td>
</tr>
<tr data-bind="if: $root.config.connectDialog.username">
<td>Username</td>
<td id="connect-dialog_input_username">Username</td>
<td><input id="username" type="text" data-bind="value: username" required></td>
</tr>
<tr data-bind="if: $root.config.connectDialog.password">
<td>Password</td>
<td id="connect-dialog_input_password">Password</td>
<td><input id="password" type="password" data-bind="value: password"></td>
</tr>
<tr data-bind="if: $root.config.connectDialog.token">
<td>Tokens</td>
<td id="connect-dialog_input_tokens">Tokens</td>
<td>
<input type="text" data-bind='value: tokenToAdd, valueUpdate: "afterkeydown"'>
</td>
@ -57,8 +57,8 @@
<tr data-bind="if: $root.config.connectDialog.token">
<td></td>
<td>
<button class="dialog-submit" type="button" data-bind="enable: selectedTokens().length > 0, click: removeSelectedTokens()">Remove</button>
<button class="dialog-submit" type="button" data-bind="enable: tokenToAdd().length > 0, click: addToken()">Add</button>
<button id="connect-dialog_controls_remove" class="dialog-submit" type="button" data-bind="enable: selectedTokens().length > 0, click: removeSelectedTokens()">Remove</button>
<button id="connect-dialog_controls_add" class="dialog-submit" type="button" data-bind="enable: tokenToAdd().length > 0, click: addToken()">Add</button>
</td>
</tr>
<tr data-bind="if: $root.config.connectDialog.token, visible: tokens().length > 0">
@ -71,8 +71,8 @@
</tr>
</table>
<div class="dialog-footer">
<input class="dialog-close" type="button" data-bind="click: hide" value="Cancel">
<input class="dialog-submit" type="submit" value="Connect">
<input id="connect-dialog_controls_cancel" class="dialog-close" type="button" data-bind="click: hide" value="Cancel">
<input id="connect-dialog_controls_connect" class="dialog-submit" type="submit" value="Connect">
</div>
</form>
</div>
@ -94,46 +94,64 @@
</div>
<form data-bind="submit: connect">
<table>
<tr>
<tr class="reason">
<td colspan=2>
<!-- ko if: type() == 0 || type() == 8 -->
The connection has been refused.
<span class="refused">
The connection has been refused.
</span>
<!-- /ko -->
<!-- ko if: type() == 1 -->
The server uses an incompatible version.
<span class="version">
The server uses an incompatible version.
</span>
<!-- /ko -->
<!-- ko if: type() == 2 -->
Your user name was rejected. Maybe try a different one?
<span class="username">
Your user name was rejected. Maybe try a different one?
</span>
<!-- /ko -->
<!-- ko if: type() == 3 -->
The given password is incorrect.
The user name you have chosen requires a special one.
<span class="userpassword">
The given password is incorrect.
The user name you have chosen requires a special one.
</span>
<!-- /ko -->
<!-- ko if: type() == 4 -->
The given password is incorrect.
<span class="serverpassword">
The given password is incorrect.
</span>
<!-- /ko -->
<!-- ko if: type() == 5 -->
The user name you have chosen is already in use.
<span class="username-in-use">
The user name you have chosen is already in use.
</span>
<!-- /ko -->
<!-- ko if: type() == 6 -->
The server is full.
<span class="full">
The server is full.
</span>
<!-- /ko -->
<!-- ko if: type() == 7 -->
The server requires you to provide a client certificate
which is not supported by this web application.
<span class="clientcert">
The server requires you to provide a client certificate
which is not supported by this web application.
</span>
<!-- /ko -->
<br>
The server reports:
<span class="server">
The server reports:
</span>
<br>
"<span class="connect-error-reason" data-bind="text: reason"></span>"
</td>
</tr>
<tr data-bind="if: type() == 2 || type() == 3 || type() == 5">
<td>Username</td>
<td class="alternate-username">Username</td>
<td><input id="username" type="text" data-bind="value: username" required></td>
</tr>
<tr data-bind="if: type() == 3 || type() == 4">
<td>Password</td>
<td class="alternate-password">Password</td>
<td><input id="password" type="password" data-bind="value: password" required></td>
</tr>
</table>
@ -284,80 +302,97 @@
<img class="avatar-view" data-bind="visible: avatarView, attr: { src: avatarView },
click: function () { avatarView(null) }"></img>
<!-- ko with: userContextMenu -->
<ul class="context-menu" data-bind="if: target,
<ul class="context-menu user-context-menu" data-bind="if: target,
style: { left: posX() + 'px',
top: posY() + 'px' }">
<!-- ko with: target -->
<li data-bind="css: { disabled: !canChangeMute() }">
<li data-bind="css: { disabled: !canChangeMute() }"
class="mute">
Mute
</li>
<li data-bind="css: { disabled: !canChangeDeafen() }">
<li data-bind="css: { disabled: !canChangeDeafen() }"
class="deafen">
Deafen
</li>
<li data-bind="css: { disabled: !canChangePrioritySpeaker() }">
<li data-bind="css: { disabled: !canChangePrioritySpeaker() }"
class="priority-speaker">
Priority Speaker
</li>
<li data-bind="css: { disabled: !canLocalMute() }">
<li data-bind="css: { disabled: !canLocalMute() }"
class="local-mute">
Local Mute
</li>
<li data-bind="css: { disabled: !canIgnoreMessages() }">
<li data-bind="css: { disabled: !canIgnoreMessages() }"
class="ignore-messages">
Ignore Messages
</li>
<li data-bind="css: { disabled: !canChangeComment() }, visible: comment">
<li data-bind="css: { disabled: !canChangeComment() }, visible: comment"
class="view-comment">
View Comment
</li>
<!-- ko if: $data === $root.thisUser() -->
<li data-bind="css: { disabled: !canChangeComment() }, visible: true">
<li data-bind="css: { disabled: !canChangeComment() }, visible: true"
class="change-comment">
Change Comment
</li>
<!-- /ko -->
<!-- ko if: $data !== $root.thisUser() -->
<li data-bind="css: { disabled: !canChangeComment() }, visible: comment">
<li data-bind="css: { disabled: !canChangeComment() }, visible: comment"
class="reset-comment">
Reset Comment
</li>
<!-- /ko -->
<li data-bind="css: { disabled: !canChangeAvatar() }, visible: texture,
click: viewAvatar">
click: viewAvatar"
class="view-avatar">
View Avatar
</li>
<!-- ko if: $data === $root.thisUser() -->
<li data-bind="css: { disabled: !canChangeAvatar() }, visible: true,
click: changeAvatar">
click: changeAvatar"
class="change-avatar">
Change Avatar
</li>
<!-- /ko -->
<li data-bind="css: { disabled: !canChangeAvatar() }, visible: texture,
click: removeAvatar">
click: removeAvatar",
class="reset-avatar">
Reset Avatar
</li>
<li data-bind="css: { disabled: true }, visible: true">
<li data-bind="css: { disabled: true }, visible: true"
class="send-message">
Send Message
</li>
<li data-bind="css: { disabled: true }, visible: true">
<li data-bind="css: { disabled: true }, visible: true"
class="information">
Information
</li>
<li data-bind="visible: $data === $root.thisUser(),
css: { checked: selfMute },
click: toggleMute">
click: toggleMute"
class="self-mute">
Self Mute
</li>
<li data-bind="visible: $data === $root.thisUser(),
css: { checked: selfDeaf },
click: toggleDeaf">
click: toggleDeaf"
class="self-deafen">
Self Deafen
</li>
<!-- ko if: $data !== $root.thisUser() -->
<li data-bind="css: { disabled: true }, visible: true">
<li data-bind="css: { disabled: true }, visible: true"
class="add-friend">
Add Friend
</li>
<li data-bind="css: { disabled: true }, visible: false">
<li data-bind="css: { disabled: true }, visible: false"
class="remove-friend">
Remove Friend
</li>
<!-- /ko -->
@ -366,43 +401,53 @@
</ul>
<!-- /ko -->
<!-- ko with: channelContextMenu -->
<ul class="context-menu" data-bind="if: target,
<ul class="context-menu channel-context-menu" data-bind="if: target,
style: { left: posX() + 'px',
top: posY() + 'px' }">
<!-- ko with: target -->
<li data-bind="visible: users.indexOf($root.thisUser()) === -1,
css: { disabled: !canJoin() },
click: $root.requestMove.bind($root, $root.thisUser())">
click: $root.requestMove.bind($root, $root.thisUser())"
class="join">
Join Channel
</li>
<li data-bind="css: { disabled: !canAdd() }">
<li data-bind="css: { disabled: !canAdd() }"
class="add">
Add
</li>
<li data-bind="css: { disabled: !canEdit() }">
<li data-bind="css: { disabled: !canEdit() }"
class="edit">
Edit
</li>
<li data-bind="css: { disabled: !canRemove() }">
<li data-bind="css: { disabled: !canRemove() }"
class="remove">
Remove
</li>
<li data-bind="css: { disabled: !canLink() }">
<li data-bind="css: { disabled: !canLink() }"
class="link">
Link
</li>
<li data-bind="css: { disabled: !canUnlink() }">
<li data-bind="css: { disabled: !canUnlink() }"
class="unlink">
Unlink
</li>
<li data-bind="css: { disabled: !canUnlink() }">
<li data-bind="css: { disabled: !canUnlink() }"
class="unlink-all">
Unlink All
</li>
<li data-bind="css: { disabled: true }">
<li data-bind="css: { disabled: true }"
class="copy-mumble-url">
Copy Mumble URL
</li>
<li data-bind="css: { disabled: true }">
<li data-bind="css: { disabled: true }"
class="copy-mumble-web-url">
Copy Mumble-Web URL
</li>
<li data-bind="css: { disabled: !canSendMessage() }">
<li data-bind="css: { disabled: !canSendMessage() }"
class="send-message">
Send Message
</li>

View file

@ -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)
@ -923,7 +924,7 @@ var ui = new GlobalBindings(window.mumbleWebConfig)
// Used only for debugging
window.mumbleUi = ui
window.onload = function () {
function initializeUI () {
var queryParams = url.parse(document.location.href, true).query
queryParams = Object.assign({}, window.mumbleWebConfig.defaults, queryParams)
var useJoinDialog = queryParams.joinDialog
@ -987,10 +988,10 @@ window.onload = function () {
}
ui.connectDialog.joinOnly(useJoinDialog)
ko.applyBindings(ui)
}
window.onresize = () => ui.updateSize()
ui.updateSize()
window.onresize = () => ui.updateSize()
ui.updateSize()
}
function log () {
console.log.apply(console, arguments)
@ -1041,18 +1042,108 @@ function userToState () {
var voiceHandler
var testVoiceHandler
initVoice(data => {
if (testVoiceHandler) {
testVoiceHandler.write(data)
}
if (!ui.client) {
if (voiceHandler) {
voiceHandler.end()
/**
* @author svartoyg
*/
function translatePiece(selector, kind, parameters, key) {
let element = document.querySelector(selector);
if (element !== null) {
const translation = translate(key);
switch (kind) {
default:
console.warn('unhandled dom translation kind "' + kind + '"');
break;
case 'textcontent':
element.textContent = translation;
break;
case 'attribute':
element.setAttribute(parameters.name || 'value', translation);
break;
}
voiceHandler = null
} else if (voiceHandler) {
voiceHandler.write(data)
} else {
console.warn(`translation selector "${selector}" for "${key}" did not match any element`)
}
}, err => {
log('Cannot initialize user media. Microphone will not work:', err)
})
}
/**
* @author svartoyg
*/
function translateEverything() {
translatePiece('#connect-dialog_title', 'textcontent', {}, 'connectdialog.title');
translatePiece('#connect-dialog_input_address', 'textcontent', {}, 'connectdialog.address');
translatePiece('#connect-dialog_input_port', 'textcontent', {}, 'connectdialog.port');
translatePiece('#connect-dialog_input_username', 'textcontent', {}, 'connectdialog.username');
translatePiece('#connect-dialog_input_password', 'textcontent', {}, 'connectdialog.password');
translatePiece('#connect-dialog_input_tokens', 'textcontent', {}, 'connectdialog.tokens');
translatePiece('#connect-dialog_controls_remove', 'textcontent', {}, 'connectdialog.remove');
translatePiece('#connect-dialog_controls_add', 'textcontent', {}, 'connectdialog.add');
translatePiece('#connect-dialog_controls_cancel', 'attribute', {'name': 'value'}, 'connectdialog.cancel');
translatePiece('#connect-dialog_controls_connect', 'attribute', {'name': 'value'}, 'connectdialog.connect');
translatePiece('.connect-dialog.error-dialog .dialog-header', 'textcontent', {}, 'connectdialog.error.title');
translatePiece('.connect-dialog.error-dialog .reason .refused', 'textcontent', {}, 'connectdialog.error.reason.refused');
translatePiece('.connect-dialog.error-dialog .reason .version', 'textcontent', {}, 'connectdialog.error.reason.version');
translatePiece('.connect-dialog.error-dialog .reason .username', 'textcontent', {}, 'connectdialog.error.reason.username');
translatePiece('.connect-dialog.error-dialog .reason .userpassword', 'textcontent', {}, 'connectdialog.error.reason.userpassword');
translatePiece('.connect-dialog.error-dialog .reason .serverpassword', 'textcontent', {}, 'connectdialog.error.reason.serverpassword');
translatePiece('.connect-dialog.error-dialog .reason .username-in-use', 'textcontent', {}, 'connectdialog.error.reason.username_in_use');
translatePiece('.connect-dialog.error-dialog .reason .full', 'textcontent', {}, 'connectdialog.error.reason.full');
translatePiece('.connect-dialog.error-dialog .reason .clientcert', 'textcontent', {}, 'connectdialog.error.reason.clientcert');
translatePiece('.connect-dialog.error-dialog .reason .server', 'textcontent', {}, 'connectdialog.error.reason.server');
translatePiece('.connect-dialog.error-dialog .alternate-username', 'textcontent', {}, 'connectdialog.username');
translatePiece('.connect-dialog.error-dialog .alternate-password', 'textcontent', {}, 'connectdialog.password');
translatePiece('.connect-dialog.error-dialog .dialog-submit', 'attribute', {'name': 'value'}, 'connectdialog.error.retry');
translatePiece('.connect-dialog.error-dialog .dialog-close', 'attribute', {'name': 'value'}, 'connectdialog.error.cancel');
translatePiece('.join-dialog .dialog-header', 'textcontent', {}, 'joindialog.title');
translatePiece('.join-dialog .dialog-submit', 'attribute', {'name': 'value'}, 'joindialog.connect');
translatePiece('.user-context-menu .mute', 'textcontent', {}, 'contextmenu.mute');
translatePiece('.user-context-menu .deafen', 'textcontent', {}, 'contextmenu.deafen');
translatePiece('.user-context-menu .priority-speaker', 'textcontent', {}, 'usercontextmenu.priority_speaker');
translatePiece('.user-context-menu .local-mute', 'textcontent', {}, 'usercontextmenu.local_mute');
translatePiece('.user-context-menu .ignore-messages', 'textcontent', {}, 'usercontextmenu.ignore_messages');
translatePiece('.user-context-menu .view-comment', 'textcontent', {}, 'usercontextmenu.view_comment');
translatePiece('.user-context-menu .change-comment', 'textcontent', {}, 'usercontextmenu.change_comment');
translatePiece('.user-context-menu .reset-comment', 'textcontent', {}, 'usercontextmenu.reset_comment');
translatePiece('.user-context-menu .view-avatar', 'textcontent', {}, 'usercontextmenu.view_avatar');
translatePiece('.user-context-menu .change-avatar', 'textcontent', {}, 'usercontextmenu.change_avatar');
translatePiece('.user-context-menu .reset-avatar', 'textcontent', {}, 'usercontextmenu.reset_avatar');
translatePiece('.user-context-menu .send-message', 'textcontent', {}, 'usercontextmenu.send_message');
translatePiece('.user-context-menu .information', 'textcontent', {}, 'usercontextmenu.information');
translatePiece('.user-context-menu .self-mute', 'textcontent', {}, 'usercontextmenu.self_mute');
translatePiece('.user-context-menu .self-deafen', 'textcontent', {}, 'usercontextmenu.self_deafen');
translatePiece('.user-context-menu .add-friend', 'textcontent', {}, 'usercontextmenu.add_friend');
translatePiece('.user-context-menu .remove-friend', 'textcontent', {}, 'usercontextmenu.remove_friend');
translatePiece('.channel-context-menu .join', 'textcontent', {}, 'channelcontextmenu.join');
translatePiece('.channel-context-menu .add', 'textcontent', {}, 'channelcontextmenu.add');
translatePiece('.channel-context-menu .edit', 'textcontent', {}, 'channelcontextmenu.edit');
translatePiece('.channel-context-menu .remove', 'textcontent', {}, 'channelcontextmenu.remove');
translatePiece('.channel-context-menu .link', 'textcontent', {}, 'channelcontextmenu.link');
translatePiece('.channel-context-menu .unlink', 'textcontent', {}, 'channelcontextmenu.unlink');
translatePiece('.channel-context-menu .unlink-all', 'textcontent', {}, 'channelcontextmenu.unlink_all');
translatePiece('.channel-context-menu .copy-mumble-url', 'textcontent', {}, 'channelcontextmenu.copy_mumble_url');
translatePiece('.channel-context-menu .copy-mumble-web-url', 'textcontent', {}, 'channelcontextmenu.copy_mumble_web_url');
translatePiece('.channel-context-menu .send-message', 'textcontent', {}, 'channelcontextmenu.send_message');
}
async function main() {
await localizationInitialize(navigator.language);
translateEverything();
initializeUI();
initVoice(data => {
if (testVoiceHandler) {
testVoiceHandler.write(data)
}
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)
})
}
window.onload = main

99
app/loc.js Normal file
View file

@ -0,0 +1,99 @@
/**
* the default language to use
*
* @var {string}
* @author svartoyg
*/
var _languageDefault = null;
/**
* the fallback language to use
*
* @var {string}
* @author svartoyg
*/
var _languageFallback = null;
/**
* two level map with ISO-639-1 code as first key and translation id as second key
*
* @var {Map<string,Map<string,string>>}
* @author svartoyg
*/
var _data = {};
/**
* @param {string} language
* @return Promise<Map<string,string>>
* @author svartoyg
*/
async function retrieveData (language) {
let json
try {
json = (await import(`../loc/${language}.json`)).default
} catch (exception) {
json = (await import(`../loc/${language.substr(0, language.indexOf('-'))}.json`)).default
}
const map = {}
flatten(json, '', map)
return map
}
function flatten (tree, prefix, result) {
for (const [key, value] of Object.entries(tree)) {
if (typeof value === 'string') {
result[prefix + key] = value
} else {
flatten(value, prefix + key + '.', result)
}
}
}
/**
* @param {string} languageDefault
* @param {string} [languageFallback]
* @author svartoyg
*/
export async function initialize (languageDefault, languageFallback = 'en') {
_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 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 svartoyg
*/
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;
}

15
loc/de.json Normal file
View file

@ -0,0 +1,15 @@
{
"connectdialog": {
"title": "Verbindung herstellen",
"address": "Adresse",
"port": "Port",
"username": "Nutzername",
"password": "Passwort",
"tokens": "Tokens",
"remove": "Entfernen",
"add": "Hinzufügen",
"cancel": "Abbrechen",
"connect": "Verbinden"
}
}

66
loc/en.json Normal file
View file

@ -0,0 +1,66 @@
{
"connectdialog": {
"title": "Connect to Server",
"address": "Address",
"port": "Port",
"username": "Username",
"password": "Password",
"tokens": "Tokens",
"remove": "Remove",
"add": "Add",
"cancel": "Cancel",
"connect": "Connect",
"error": {
"title": "Failed to connect",
"reason": {
"refused": "The connection has been refused.",
"version": "The server uses an incompatible version.",
"username": "Your user name was rejected. Maybe try a different one?",
"userpassword": "The given password is incorrect.\nThe user name you have chosen requires a special one.",
"serverpassword": "The given password is incorrect.",
"username_in_use": "The user name you have chosen is already in use.",
"full": "The server is full.",
"clientcert": "The server requires you to provide a client certificate which is not supported by this web application.",
"server": "The server reports:"
},
"retry": "Retry",
"cancel": "Cancel"
}
},
"joindialog": {
"title": "Mumble Voice Conference",
"connect": "Join Conference"
},
"usercontextmenu": {
"mute": "Mute",
"deafen": "Deafen",
"priority_speaker": "Priority Speaker",
"local_mute": "Local Mute",
"ignore_messages": "Ignore Messages",
"view_comment": "View Comment",
"change_comment": "Change Comment",
"reset_comment": "Reset Comment",
"view_avatar": "View Avatar",
"change_avatar": "Change Avatar",
"reset_avatar": "Reset Avatar",
"send_message": "Send Message",
"information": "Information",
"self_mute": "Self Mute",
"self_deafen": "Self Deafen",
"add_friend": "Add Friend",
"remove_friend": "Remove Friend"
},
"channelcontextmenu": {
"channelcontextmenu.join": "Join Channel",
"channelcontextmenu.add": "Add",
"channelcontextmenu.edit": "Edit",
"channelcontextmenu.remove": "Remove",
"channelcontextmenu.link": "Link",
"channelcontextmenu.unlink": "Unlink",
"channelcontextmenu.unlink_all": "Unlink All",
"channelcontextmenu.copy_mumble_url": "Copy Mumble URL",
"channelcontextmenu.copy_mumble_web_url": "Copy Mumble-Web URL",
"channelcontextmenu.send_message": "Send Message"
}
}

15
loc/eo.json Normal file
View file

@ -0,0 +1,15 @@
{
"connectdialog": {
"title": "Konektado",
"address": "Adreso",
"port": "Pordo",
"username": "Uzantnomo",
"password": "Pasvorto",
"tokens": "Ĵetonoj",
"remove": "Forigi",
"add": "Aldoni",
"cancel": "Nuligi",
"connect": "Konekti"
}
}

View file

@ -15,6 +15,7 @@ module.exports = {
devtool: "cheap-source-map",
output: {
path: path.join(__dirname, 'dist'),
chunkFilename: '[chunkhash].js',
filename: '[name].js'
},
module: {