Finalizing the translation system.
Needs some tests and a final translation generation. As well a handy translation mapper or editor would be likely. May source this out into another project?
This commit is contained in:
parent
24b220a966
commit
6e82161334
8 changed files with 733 additions and 63 deletions
|
@ -98,3 +98,150 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal .settings-translations {
|
||||||
|
margin: 5px;
|
||||||
|
.setting-list {
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: start;
|
||||||
|
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
border: solid 1px lightgray;
|
||||||
|
padding: 2px;
|
||||||
|
background: #33333318;
|
||||||
|
|
||||||
|
height: 50%;
|
||||||
|
min-height: 50%;
|
||||||
|
max-height: 50%;
|
||||||
|
|
||||||
|
.entry {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
.default { }
|
||||||
|
|
||||||
|
.name {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.translation:not(.default) {
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.translation {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.repository {
|
||||||
|
.name {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background: #0000FF77;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.button {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #00000010;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.management {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
margin-top: 5px;
|
||||||
|
float: right;
|
||||||
|
|
||||||
|
.space {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.restart-note {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The info modal for the translations */
|
||||||
|
.entry-info-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.property {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
.key {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.property-repository {
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
margin-right: 5px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #00000011;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.property-contributors {
|
||||||
|
.value {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.contributor {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,16 +13,16 @@
|
||||||
<div style="height: 45px; width: 100%; border-radius: 2px 0px 0px 0px; border-bottom-width: 0px; background-color: lightgrey"
|
<div style="height: 45px; width: 100%; border-radius: 2px 0px 0px 0px; border-bottom-width: 0px; background-color: lightgrey"
|
||||||
class="main_container">
|
class="main_container">
|
||||||
<div id="control_bar" class="control_bar">
|
<div id="control_bar" class="control_bar">
|
||||||
<div class="button btn_connect" title="Connect to a server">
|
<div class="button btn_connect" title="{{tr 'Connect to a server' /}}">
|
||||||
<div class="icon_x32 client-connect"></div>
|
<div class="icon_x32 client-connect"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="button btn_disconnect" title="Disconnect from server" style="display: none">
|
<div class="button btn_disconnect" title="{{tr 'Disconnect from server' /}}" style="display: none">
|
||||||
<div class="icon_x32 client-disconnect"></div>
|
<div class="icon_x32 client-disconnect"></div>
|
||||||
</div>
|
</div>
|
||||||
<!--<div class="button btn_disconnect"><div class="icon_x32 client-disconnect"></div></div>-->
|
<!--<div class="button btn_disconnect"><div class="icon_x32 client-disconnect"></div></div>-->
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
<div class="button-dropdown btn_away" title="Toggle away status">
|
<div class="button-dropdown btn_away" title="{{tr 'Toggle away status' /}}">
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<div class="button icon_x32 client-away btn_away_toggle"></div>
|
<div class="button icon_x32 client-away btn_away_toggle"></div>
|
||||||
<div class="button-dropdown">
|
<div class="button-dropdown">
|
||||||
|
@ -30,19 +30,19 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<div class="btn_away_toggle"><div class="icon client-away"></div><a>Toggle away status</a></div>
|
<div class="btn_away_toggle"><div class="icon client-away"></div><a>{{tr "Toggle away status" /}}</a></div>
|
||||||
<div class="btn_away_message"><div class="icon client-away"></div><a>Set away message</a></div>
|
<div class="btn_away_message"><div class="icon client-away"></div><a>{{ŧr "Set away message" /}}</a></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="button btn_mute_input">
|
<div class="button btn_mute_input">
|
||||||
<div class="icon_x32 client-input_muted" title="Mute/unmute microphone"></div>
|
<div class="icon_x32 client-input_muted" title="{{tr 'Mute/unmute microphone' /}}"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="button btn_mute_output">
|
<div class="button btn_mute_output">
|
||||||
<div class="icon_x32 client-output_muted" title="Mute/unmute headphones"></div>
|
<div class="icon_x32 client-output_muted" title="{{tr 'Mute/unmute headphones' /}}"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
<div class="button-dropdown btn_token" title="Use token">
|
<div class="button-dropdown btn_token" title="{{tr 'Use token' /}}">
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<div class="button icon_x32 client-token btn_token_use"></div>
|
<div class="button icon_x32 client-token btn_token_use"></div>
|
||||||
<div class="button-dropdown">
|
<div class="button-dropdown">
|
||||||
|
@ -50,20 +50,20 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<div class="btn_token_list"><div class="icon client-token"></div><a>List tokens</a></div>
|
<div class="btn_token_list"><div class="icon client-token"></div><a>{{tr "List tokens" /}}</a></div>
|
||||||
<div class="btn_token_use"><div class="icon client-token_use"></div><a>Use token</a></div>
|
<div class="btn_token_use"><div class="icon client-token_use"></div><a>{{tr "Use token" /}}</a></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="width: 100%"></div>
|
<div style="width: 100%"></div>
|
||||||
<div class="button btn_banlist" title="Banlist">
|
<div class="button btn_banlist" title="{{tr 'Banlist' /}}">
|
||||||
<div class="icon_x32 client-ban_list"></div>
|
<div class="icon_x32 client-ban_list"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="button btn_permissions" title="View/edit permissions">
|
<div class="button btn_permissions" title="{{tr 'View/edit permissions' /}}">
|
||||||
<div class="icon_x32 client-permission_overview"></div>
|
<div class="icon_x32 client-permission_overview"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="button btn_open_settings" title="Edit global client settings">
|
<div class="button btn_open_settings" title="{{tr 'Edit global client settings' /}}">
|
||||||
<div class="icon_x32 client-settings"></div>
|
<div class="icon_x32 client-settings"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -661,8 +661,124 @@
|
||||||
</div>
|
</div>
|
||||||
</x-content>
|
</x-content>
|
||||||
</x-entry>
|
</x-entry>
|
||||||
|
<x-entry>
|
||||||
|
<x-tag>
|
||||||
|
{{tr "Translations" /}}
|
||||||
|
</x-tag>
|
||||||
|
<x-content>
|
||||||
|
<div class="settings-translations">
|
||||||
|
<div class="group_box">
|
||||||
|
<div class="header">{{tr "Available translations" /}}</div>
|
||||||
|
<div class="content settings-microphone">
|
||||||
|
<div class="setting-list">
|
||||||
|
<div class="list">
|
||||||
|
<!--
|
||||||
|
<div class="entry default">{{tr "English (Default / Fallback)" /}}</div>
|
||||||
|
<div class="entry repository">
|
||||||
|
<div class="name">TeaSpeak Official</div>
|
||||||
|
<div class="button button-delete"><div class="icon client-delete"></div></div>
|
||||||
|
<div class="button button-info"><div class="icon client-about"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="entry translation selected">
|
||||||
|
<div class="name">German (Google Translate)</div>
|
||||||
|
<div class="button button-info"><div class="icon client-about"></div></div>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
</div>
|
||||||
|
<div class="management">
|
||||||
|
<div class="loading">Loading...</div><div class="space"></div><button class="button-add-repository">{{tr "Add repository" /}}</button>
|
||||||
|
</div>
|
||||||
|
<div class="restart-note">
|
||||||
|
<p>
|
||||||
|
{{tr "Attention: These settings get only affected after a restart or reload!" /}}
|
||||||
|
</p>
|
||||||
|
<button class="button-reload">{{tr "reload now" /}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-content>
|
||||||
|
</x-entry>
|
||||||
</x-tab>
|
</x-tab>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script class="jsrender-template" id="settings-translations-list-entry" type="text/html">
|
||||||
|
{{if type == "repository" }}
|
||||||
|
<div class="entry repository" repository="{{:id}}">
|
||||||
|
<div class="name">{{> name}}</div>
|
||||||
|
<div class="button button-delete"><div class="icon client-delete"></div></div>
|
||||||
|
<div class="button button-info"><div class="icon client-about"></div></div>
|
||||||
|
</div>
|
||||||
|
{{else type == "default" }}
|
||||||
|
<div class="entry default {{if selected}}selected{{/if}}">{{tr "English (Default / Fallback)" /}}</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="entry translation {{if selected}}selected{{/if}}" parent-repository="{{:id}}">
|
||||||
|
<div class="name">{{> name}}</div>
|
||||||
|
<div class="button button-info"><div class="icon client-about"></div></div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script class="jsrender-template" id="settings-translations-list-entry-info" type="text/html">
|
||||||
|
<div class="entry-info-container">
|
||||||
|
{{if type == "repository" }}
|
||||||
|
<!--
|
||||||
|
unique_id: string;
|
||||||
|
url: string;
|
||||||
|
name?: string;
|
||||||
|
contact?: string;
|
||||||
|
translations?: RepositoryTranslation[];
|
||||||
|
load_timestamp?: number;
|
||||||
|
-->
|
||||||
|
<div class="property property-name"><div class="key">Name:</div><div class="value">{{>name}}</div></div>
|
||||||
|
<div class="property property-url"><div class="key">URL:</div><div class="value">{{>url}}</div></div>
|
||||||
|
<div class="property property-contact"><div class="key">Contact:</div><div class="value">{{>contact}}</div></div>
|
||||||
|
<div class="property property-translations"><div class="key">Translations:</div><div class="value">{{:translations.length}}</div></div>
|
||||||
|
{{else}}
|
||||||
|
<!--
|
||||||
|
name: string;
|
||||||
|
contributors: Contributor[];
|
||||||
|
-->
|
||||||
|
<div class="property property-name"><div class="key">Name:</div><div class="value">{{>name}}</div></div>
|
||||||
|
<div class="property property-url"><div class="key">URL:</div><div class="value">{{>url}}</div></div>
|
||||||
|
<div class="property property-repository">
|
||||||
|
<div class="key">Repository:</div>
|
||||||
|
<div class="value">
|
||||||
|
<p>{{>repository_name}}</p>
|
||||||
|
<div class="button button-info"><div class="icon client-about"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{if !contributors }}
|
||||||
|
{{else contributors.length == 1}}
|
||||||
|
<div class="property property-contributor">
|
||||||
|
<div class="key">Contributor:</div>
|
||||||
|
<div class="value">
|
||||||
|
{{>contributors[0].name}}
|
||||||
|
{{if contributors[0].email}}
|
||||||
|
 <{{>contributors[0].email}}>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="property property-contributors">
|
||||||
|
<div class="key">Contributors:</div>
|
||||||
|
<div class="value">
|
||||||
|
{{for contributors}}
|
||||||
|
<div class="contributor">
|
||||||
|
{{>name}}
|
||||||
|
{{if email}}
|
||||||
|
 <{{>email}}>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/for}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
<script class="jsrender-template" id="tmpl_change_volume" type="text/html">
|
<script class="jsrender-template" id="tmpl_change_volume" type="text/html">
|
||||||
<div style="display: flex; justify-content: center; vertical-align: center">
|
<div style="display: flex; justify-content: center; vertical-align: center">
|
||||||
<input type="range" min="0" max="200" value="100" class="volume_slider" style="width: 100%">
|
<input type="range" min="0" max="200" value="100" class="volume_slider" style="width: 100%">
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
{
|
{
|
||||||
"info": {
|
"info": {
|
||||||
"name": "Auto translated messages for language de",
|
"name": "Auto translated messages for language de by the google translator",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
{
|
{
|
||||||
"name": "Google Translate, via script by Markus Hadenfeldt",
|
"name": "Google Translate, via script by Markus Hadenfeldt",
|
||||||
"email": "gtr.i18n.client@teaspeak.de"
|
"email": "gtr.i18n.client@teaspeak.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Markus Hadenfeldt",
|
||||||
|
"email": "i18n.client@teaspeak.de"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
|
@ -3,6 +3,12 @@
|
||||||
{
|
{
|
||||||
"key": "de_DE",
|
"key": "de_DE",
|
||||||
"path": "de_DE.translation"
|
"path": "de_DE.translation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "de_DE_gt",
|
||||||
|
"path": "de_DE_google_translate.translation"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"name": "Default TeaSpeak repository",
|
||||||
|
"contact": "i18n@teaspeak.de"
|
||||||
}
|
}
|
|
@ -323,6 +323,8 @@ class ChatBox {
|
||||||
}
|
}
|
||||||
globalClient.serverConnection.sendMessage(text, ChatType.SERVER);
|
globalClient.serverConnection.sendMessage(text, ChatType.SERVER);
|
||||||
};
|
};
|
||||||
|
this.serverChat().name = tr("Server chat");
|
||||||
|
|
||||||
this.createChat("chat_channel", ChatType.CHANNEL).onMessageSend = (text: string) => {
|
this.createChat("chat_channel", ChatType.CHANNEL).onMessageSend = (text: string) => {
|
||||||
if(!globalClient.serverConnection) {
|
if(!globalClient.serverConnection) {
|
||||||
chat.channelChat().appendError(tr("Could not send chant message (Not connected)"));
|
chat.channelChat().appendError(tr("Could not send chant message (Not connected)"));
|
||||||
|
@ -331,6 +333,7 @@ class ChatBox {
|
||||||
|
|
||||||
globalClient.serverConnection.sendMessage(text, ChatType.CHANNEL, globalClient.getClient().currentChannel());
|
globalClient.serverConnection.sendMessage(text, ChatType.CHANNEL, globalClient.getClient().currentChannel());
|
||||||
};
|
};
|
||||||
|
this.channelChat().name = tr("Channel chat");
|
||||||
|
|
||||||
globalClient.permissions.initializedListener.push(flag => {
|
globalClient.permissions.initializedListener.push(flag => {
|
||||||
if(flag) this.activeChat0(this._activeChat);
|
if(flag) this.activeChat0(this._activeChat);
|
||||||
|
|
|
@ -11,6 +11,15 @@
|
||||||
"verified"
|
"verified"
|
||||||
]
|
]
|
||||||
*/
|
*/
|
||||||
|
function guid() {
|
||||||
|
function s4() {
|
||||||
|
return Math.floor((1 + Math.random()) * 0x10000)
|
||||||
|
.toString(16)
|
||||||
|
.substring(1);
|
||||||
|
}
|
||||||
|
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
|
||||||
|
}
|
||||||
|
|
||||||
namespace i18n {
|
namespace i18n {
|
||||||
interface TranslationKey {
|
interface TranslationKey {
|
||||||
message: string;
|
message: string;
|
||||||
|
@ -36,10 +45,26 @@ namespace i18n {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TranslationFile {
|
interface TranslationFile {
|
||||||
|
url: string;
|
||||||
|
|
||||||
info: FileInfo;
|
info: FileInfo;
|
||||||
translations: Translation[];
|
translations: Translation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RepositoryTranslation {
|
||||||
|
key: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TranslationRepository {
|
||||||
|
unique_id: string;
|
||||||
|
url: string;
|
||||||
|
name?: string;
|
||||||
|
contact?: string;
|
||||||
|
translations?: RepositoryTranslation[];
|
||||||
|
load_timestamp?: number;
|
||||||
|
}
|
||||||
|
|
||||||
let translations: Translation[] = [];
|
let translations: Translation[] = [];
|
||||||
let fast_translate: { [key:string]:string; } = {};
|
let fast_translate: { [key:string]:string; } = {};
|
||||||
export function tr(message: string, key?: string) {
|
export function tr(message: string, key?: string) {
|
||||||
|
@ -60,65 +85,238 @@ namespace i18n {
|
||||||
return translated;
|
return translated;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function load_file(url: string) : Promise<void> {
|
async function load_translation_file(url: string) : Promise<TranslationFile> {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<TranslationFile>((resolve, reject) => {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
async: true,
|
async: true,
|
||||||
success: result => {
|
success: result => {
|
||||||
console.dir(result);
|
try {
|
||||||
const file = (typeof(result) === "string" ? JSON.parse(result) : result) as TranslationFile;
|
console.dir(result);
|
||||||
if(!file) {
|
const file = (typeof(result) === "string" ? JSON.parse(result) : result) as TranslationFile;
|
||||||
reject("Invalid json");
|
if(!file) {
|
||||||
return;
|
reject("Invalid json");
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
//TODO validate file
|
file.url = url;
|
||||||
translations = file.translations;
|
//TODO validate file
|
||||||
log.info(LogCategory.I18N, tr("Successfully initialized up translation file from %s"), url);
|
resolve(file);
|
||||||
resolve();
|
} catch(error) {
|
||||||
|
log.warn(LogCategory.I18N, tr("Failed to load translation file %s. Failed to parse or process json: %o"), url, error);
|
||||||
|
reject(tr("Failed to process or parse json!"));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: (xhr, error) => {
|
error: (xhr, error) => {
|
||||||
log.warn(LogCategory.I18N, "Failed to load translation file from \"%s\". Error: %o", url, error);
|
reject(tr("Failed to load file: ") + error);
|
||||||
reject("Failed to load file: " + error);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function load_file(url: string) : Promise<void> {
|
||||||
|
return load_translation_file(url).then(result => {
|
||||||
|
log.info(LogCategory.I18N, tr("Successfully initialized up translation file from %s"), url);
|
||||||
|
translations = result.translations;
|
||||||
|
return Promise.resolve();
|
||||||
|
}).catch(error => {
|
||||||
|
log.warn(LogCategory.I18N, tr("Failed to load translation file from \"%s\". Error: %o"), url, error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load_repository0(repo: TranslationRepository, reload: boolean) {
|
||||||
|
if(!repo.load_timestamp || repo.load_timestamp < 1000 || reload) {
|
||||||
|
const info_json = await new Promise((resolve, reject) => {
|
||||||
|
$.ajax({
|
||||||
|
url: repo.url + "/info.json",
|
||||||
|
async: true,
|
||||||
|
cache: !reload,
|
||||||
|
success: result => {
|
||||||
|
const file = (typeof(result) === "string" ? JSON.parse(result) : result) as TranslationFile;
|
||||||
|
if(!file) {
|
||||||
|
reject("Invalid json");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(file);
|
||||||
|
},
|
||||||
|
error: (xhr, error) => {
|
||||||
|
reject(tr("Failed to load file: ") + error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(repo, info_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!repo.unique_id)
|
||||||
|
repo.unique_id = guid();
|
||||||
|
|
||||||
|
repo.translations = repo.translations || [];
|
||||||
|
repo.load_timestamp = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function load_repository(url: string) : Promise<TranslationRepository> {
|
||||||
|
const result = {} as TranslationRepository;
|
||||||
|
result.url = url;
|
||||||
|
await load_repository0(result, false);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace config {
|
||||||
|
export interface TranslationConfig {
|
||||||
|
current_repository_url?: string;
|
||||||
|
current_language?: string;
|
||||||
|
|
||||||
|
current_translation_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RepositoryConfig {
|
||||||
|
repositories?: {
|
||||||
|
url?: string;
|
||||||
|
repository?: TranslationRepository;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const repository_config_key = "i18n.repository";
|
||||||
|
let _cached_repository_config: RepositoryConfig;
|
||||||
|
export function repository_config() {
|
||||||
|
if(_cached_repository_config)
|
||||||
|
return _cached_repository_config;
|
||||||
|
|
||||||
|
const config_string = localStorage.getItem(repository_config_key);
|
||||||
|
const config: RepositoryConfig = config_string ? JSON.parse(config_string) : {};
|
||||||
|
config.repositories = config.repositories || [];
|
||||||
|
for(const repo of config.repositories)
|
||||||
|
(repo.repository || {load_timestamp: 0}).load_timestamp = 0;
|
||||||
|
|
||||||
|
return _cached_repository_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function save_repository_config() {
|
||||||
|
localStorage.setItem(repository_config_key, JSON.stringify(_cached_repository_config));
|
||||||
|
}
|
||||||
|
|
||||||
|
const translation_config_key = "i18n.translation";
|
||||||
|
let _cached_translation_config: TranslationConfig;
|
||||||
|
|
||||||
|
export function translation_config() : TranslationConfig {
|
||||||
|
if(_cached_translation_config)
|
||||||
|
return _cached_translation_config;
|
||||||
|
|
||||||
|
const config_string = localStorage.getItem(translation_config_key);
|
||||||
|
_cached_translation_config = config_string ? JSON.parse(config_string) : {};
|
||||||
|
return _cached_translation_config;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function save_translation_config() {
|
||||||
|
localStorage.setItem(translation_config_key, JSON.stringify(_cached_translation_config));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function register_repository(repository: TranslationRepository) {
|
||||||
|
if(!repository) return;
|
||||||
|
|
||||||
|
for(const repo of config.repository_config().repositories)
|
||||||
|
if(repo.url == repository.url) return;
|
||||||
|
|
||||||
|
config.repository_config().repositories.push(repository);
|
||||||
|
config.save_repository_config();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registered_repositories() : TranslationRepository[] {
|
||||||
|
return config.repository_config().repositories.map(e => e.repository || {url: e.url, load_timestamp: 0} as TranslationRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function delete_repository(repository: TranslationRepository) {
|
||||||
|
if(!repository) return;
|
||||||
|
|
||||||
|
for(const repo of [...config.repository_config().repositories])
|
||||||
|
if(repo.url == repository.url) {
|
||||||
|
config.repository_config().repositories.remove(repo);
|
||||||
|
}
|
||||||
|
config.save_repository_config();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function iterate_translations(callback_entry: (repository: TranslationRepository, entry: TranslationFile) => any, callback_finish: () => any) {
|
||||||
|
let count = 0;
|
||||||
|
const update_finish = () => {
|
||||||
|
console.error(count);
|
||||||
|
if(count == 0 && callback_finish)
|
||||||
|
callback_finish();
|
||||||
|
};
|
||||||
|
|
||||||
|
for(const repo of registered_repositories()) {
|
||||||
|
count++;
|
||||||
|
load_repository0(repo, false).then(() => {
|
||||||
|
for(const translation of repo.translations || []) {
|
||||||
|
const translation_path = repo.url + "/" + translation.path;
|
||||||
|
count++;
|
||||||
|
|
||||||
|
load_translation_file(translation_path).then(file => {
|
||||||
|
if(callback_entry) {
|
||||||
|
try {
|
||||||
|
callback_entry(repo, file);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
//TODO more error handling?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
count--;
|
||||||
|
update_finish();
|
||||||
|
}).catch(error => {
|
||||||
|
log.warn(LogCategory.I18N, tr("Failed to load translation file for repository %s. Translation: %s (%s) Error: %o"), repo.name, translation.key, translation_path, error);
|
||||||
|
|
||||||
|
count--;
|
||||||
|
update_finish();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
count--;
|
||||||
|
update_finish();
|
||||||
|
}).catch(error => {
|
||||||
|
log.warn(LogCategory.I18N, tr("Failed to load repository while iteration: %s (%s). Error: %o"), (repo || {name: "unknown"}).name, (repo || {url: "unknown"}).url, error);
|
||||||
|
|
||||||
|
count--;
|
||||||
|
update_finish();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
update_finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function select_translation(repository: TranslationRepository, entry: TranslationFile) {
|
||||||
|
const cfg = config.translation_config();
|
||||||
|
|
||||||
|
if(entry && repository) {
|
||||||
|
cfg.current_language = entry.info.name;
|
||||||
|
cfg.current_repository_url = repository.url;
|
||||||
|
cfg.current_translation_url = entry.url;
|
||||||
|
} else {
|
||||||
|
cfg.current_language = undefined;
|
||||||
|
cfg.current_repository_url = undefined;
|
||||||
|
cfg.current_translation_url = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.save_translation_config();
|
||||||
|
}
|
||||||
|
|
||||||
export async function initialize() {
|
export async function initialize() {
|
||||||
// await load_file("http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/web/environment/development/i18n/de_DE.translation");
|
const cfg = config.translation_config();
|
||||||
await load_file("http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/web/environment/development/i18n/test.json");
|
|
||||||
|
if(cfg.current_translation_url) {
|
||||||
|
try {
|
||||||
|
await load_file(cfg.current_translation_url);
|
||||||
|
} catch (error) {
|
||||||
|
createErrorModal(tr("Translation System"), tr("Failed to load current selected translation file.") + "<br>File: " + cfg.current_translation_url + "<br>Error: " + error + "<br>" + tr("Using default fallback translations.")).open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// await load_file("http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/web/environment/development/i18n/de_DE.translation");
|
||||||
|
// await load_file("http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/web/environment/development/i18n/test.json");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tr: typeof i18n.tr = i18n.tr;
|
const tr: typeof i18n.tr = i18n.tr;
|
||||||
|
|
||||||
/*
|
|
||||||
{
|
|
||||||
"info": {
|
|
||||||
"contributors": [
|
|
||||||
{
|
|
||||||
"name": "Markus Hadenfeldt",
|
|
||||||
"email": "i18n.client@teaspeak.de"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"name": "German translations"
|
|
||||||
},
|
|
||||||
"translations": [
|
|
||||||
{
|
|
||||||
"key": {
|
|
||||||
"message": "Show permission description",
|
|
||||||
"line": 374,
|
|
||||||
"character": 30,
|
|
||||||
"filename": "/home/wolverindev/TeaSpeak/TeaSpeak/Web-Client/shared/js/ui/modal/ModalPermissionEdit.ts"
|
|
||||||
},
|
|
||||||
"translated": "Berechtigungsbeschreibung anzeigen",
|
|
||||||
"flags": [
|
|
||||||
"google-translate",
|
|
||||||
"verified"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
*/
|
|
|
@ -92,7 +92,30 @@ function setup_jsrender() : boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initialize() {
|
async function initialize() {
|
||||||
if(!setup_jsrender()) return;
|
const display_load_error = message => {
|
||||||
|
if(typeof(display_critical_load) !== "undefined")
|
||||||
|
display_critical_load(message);
|
||||||
|
else
|
||||||
|
displayCriticalError(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(!setup_jsrender())
|
||||||
|
throw "invalid load";
|
||||||
|
} catch (error) {
|
||||||
|
display_load_error(tr("Failed to setup jsrender"));
|
||||||
|
console.error(tr("Failed to load jsrender! %o"), error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try { //Initialize main template
|
||||||
|
const main = $("#tmpl_main").renderTag();
|
||||||
|
$("body").append(main);
|
||||||
|
} catch(error) {
|
||||||
|
display_load_error(tr("Failed to setup main page!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await i18n.initialize();
|
await i18n.initialize();
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
/// <reference path="../../voice/AudioController.ts" />
|
/// <reference path="../../voice/AudioController.ts" />
|
||||||
|
|
||||||
namespace Modals {
|
namespace Modals {
|
||||||
|
import info = log.info;
|
||||||
|
import TranslationRepository = i18n.TranslationRepository;
|
||||||
|
|
||||||
export function spawnSettingsModal() {
|
export function spawnSettingsModal() {
|
||||||
let modal;
|
let modal;
|
||||||
modal = createModal({
|
modal = createModal({
|
||||||
|
@ -12,6 +15,7 @@ namespace Modals {
|
||||||
let template = $("#tmpl_settings").renderTag();
|
let template = $("#tmpl_settings").renderTag();
|
||||||
template = $.spawn("div").append(template);
|
template = $.spawn("div").append(template);
|
||||||
initialiseSettingListeners(modal,template = template.tabify());
|
initialiseSettingListeners(modal,template = template.tabify());
|
||||||
|
initialise_translations(template.find(".settings-translations"));
|
||||||
return template;
|
return template;
|
||||||
},
|
},
|
||||||
footer: () => {
|
footer: () => {
|
||||||
|
@ -22,7 +26,7 @@ namespace Modals {
|
||||||
footer.css("text-align", "right");
|
footer.css("text-align", "right");
|
||||||
|
|
||||||
let buttonOk = $.spawn("button");
|
let buttonOk = $.spawn("button");
|
||||||
buttonOk.text("Ok");
|
buttonOk.text(tr("Ok"));
|
||||||
buttonOk.click(() => modal.close());
|
buttonOk.click(() => modal.close());
|
||||||
footer.append(buttonOk);
|
footer.append(buttonOk);
|
||||||
|
|
||||||
|
@ -270,4 +274,173 @@ namespace Modals {
|
||||||
//Initialise speakers
|
//Initialise speakers
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initialise_translations(tag: JQuery) {
|
||||||
|
{ //Initialize the list
|
||||||
|
const tag_list = tag.find(".setting-list .list");
|
||||||
|
const tag_loading = tag.find(".setting-list .loading");
|
||||||
|
const template = $("#settings-translations-list-entry");
|
||||||
|
const restart_hint = tag.find(".setting-list .restart-note");
|
||||||
|
restart_hint.hide();
|
||||||
|
|
||||||
|
const update_list = () => {
|
||||||
|
tag_list.empty();
|
||||||
|
|
||||||
|
const currently_selected = i18n.config.translation_config().current_translation_url;
|
||||||
|
{ //Default translation
|
||||||
|
const tag = template.renderTag({
|
||||||
|
type: "default",
|
||||||
|
selected: !currently_selected || currently_selected == "default"
|
||||||
|
});
|
||||||
|
tag.on('click', () => {
|
||||||
|
i18n.select_translation(undefined, undefined);
|
||||||
|
tag_list.find(".selected").removeClass("selected");
|
||||||
|
tag.addClass("selected");
|
||||||
|
|
||||||
|
restart_hint.show();
|
||||||
|
});
|
||||||
|
tag.appendTo(tag_list);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const display_repository_info = (repository: TranslationRepository) => {
|
||||||
|
const info_modal = createModal({
|
||||||
|
header: tr("Repository info"),
|
||||||
|
body: () => {
|
||||||
|
return $("#settings-translations-list-entry-info").renderTag({
|
||||||
|
type: "repository",
|
||||||
|
name: repository.name,
|
||||||
|
url: repository.url,
|
||||||
|
contact: repository.contact,
|
||||||
|
translations: repository.translations || []
|
||||||
|
});
|
||||||
|
},
|
||||||
|
footer: () => {
|
||||||
|
let footer = $.spawn("div");
|
||||||
|
footer.addClass("modal-button-group");
|
||||||
|
footer.css("margin-top", "5px");
|
||||||
|
footer.css("margin-bottom", "5px");
|
||||||
|
footer.css("text-align", "right");
|
||||||
|
|
||||||
|
let buttonOk = $.spawn("button");
|
||||||
|
buttonOk.text(tr("Close"));
|
||||||
|
buttonOk.click(() => info_modal.close());
|
||||||
|
footer.append(buttonOk);
|
||||||
|
|
||||||
|
return footer;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
info_modal.open()
|
||||||
|
};
|
||||||
|
|
||||||
|
tag_loading.show();
|
||||||
|
i18n.iterate_translations((repo, entry) => {
|
||||||
|
let repo_tag = tag_list.find("[repository=\"" + repo.unique_id + "\"]");
|
||||||
|
if(repo_tag.length == 0) {
|
||||||
|
repo_tag = template.renderTag({
|
||||||
|
type: "repository",
|
||||||
|
name: repo.name || repo.url,
|
||||||
|
id: repo.unique_id
|
||||||
|
});
|
||||||
|
|
||||||
|
repo_tag.find(".button-delete").on('click', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
Modals.spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this repository?"), answer => {
|
||||||
|
if(answer) {
|
||||||
|
i18n.delete_repository(repo);
|
||||||
|
update_list();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
repo_tag.find(".button-info").on('click', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
display_repository_info(repo);
|
||||||
|
});
|
||||||
|
|
||||||
|
tag_list.append(repo_tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tag = template.renderTag({
|
||||||
|
type: "translation",
|
||||||
|
name: entry.info.name || entry.url,
|
||||||
|
id: repo.unique_id,
|
||||||
|
selected: i18n.config.translation_config().current_translation_url == entry.url
|
||||||
|
});
|
||||||
|
tag.find(".button-info").on('click', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const info_modal = createModal({
|
||||||
|
header: tr("Translation info"),
|
||||||
|
body: () => {
|
||||||
|
const tag = $("#settings-translations-list-entry-info").renderTag({
|
||||||
|
type: "translation",
|
||||||
|
name: entry.info.name,
|
||||||
|
url: entry.url,
|
||||||
|
repository_name: repo.name,
|
||||||
|
contributors: entry.info.contributors || []
|
||||||
|
});
|
||||||
|
|
||||||
|
tag.find(".button-info").on('click', () => display_repository_info(repo));
|
||||||
|
|
||||||
|
return tag;
|
||||||
|
},
|
||||||
|
footer: () => {
|
||||||
|
let footer = $.spawn("div");
|
||||||
|
footer.addClass("modal-button-group");
|
||||||
|
footer.css("margin-top", "5px");
|
||||||
|
footer.css("margin-bottom", "5px");
|
||||||
|
footer.css("text-align", "right");
|
||||||
|
|
||||||
|
let buttonOk = $.spawn("button");
|
||||||
|
buttonOk.text(tr("Close"));
|
||||||
|
buttonOk.click(() => info_modal.close());
|
||||||
|
footer.append(buttonOk);
|
||||||
|
|
||||||
|
return footer;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
info_modal.open()
|
||||||
|
});
|
||||||
|
tag.on('click', e => {
|
||||||
|
if(e.isDefaultPrevented()) return;
|
||||||
|
i18n.select_translation(repo, entry);
|
||||||
|
tag_list.find(".selected").removeClass("selected");
|
||||||
|
tag.addClass("selected");
|
||||||
|
|
||||||
|
restart_hint.show();
|
||||||
|
});
|
||||||
|
tag.insertAfter(repo_tag)
|
||||||
|
}, () => {
|
||||||
|
tag_loading.hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
tag.find(".button-add-repository").on('click', () => {
|
||||||
|
createInputModal("Enter URL", tr("Enter repository URL:<br>"), text => true, url => { //FIXME test valid url
|
||||||
|
if(!url) return;
|
||||||
|
|
||||||
|
tag_loading.show();
|
||||||
|
i18n.load_repository(url as string).then(repository => {
|
||||||
|
i18n.register_repository(repository);
|
||||||
|
update_list();
|
||||||
|
}).catch(error => {
|
||||||
|
tag_loading.hide();
|
||||||
|
createErrorModal("Failed to load repository", tr("Failed to query repository.<br>Ensure that this repository is valid and reachable.<br>Error: ") + error).open();
|
||||||
|
})
|
||||||
|
}).open();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_hint.find(".button-reload").on('click', () => {
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
update_list();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue