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
|
@ -97,4 +97,151 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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"
|
||||
class="main_container">
|
||||
<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>
|
||||
<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>
|
||||
<!--<div class="button btn_disconnect"><div class="icon_x32 client-disconnect"></div></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="button icon_x32 client-away btn_away_toggle"></div>
|
||||
<div class="button-dropdown">
|
||||
|
@ -30,19 +30,19 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<div class="btn_away_toggle"><div class="icon client-away"></div><a>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_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>{{ŧr "Set away message" /}}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<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 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 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="button icon_x32 client-token btn_token_use"></div>
|
||||
<div class="button-dropdown">
|
||||
|
@ -50,20 +50,20 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<div class="btn_token_list"><div class="icon client-token"></div><a>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_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>{{tr "Use token" /}}</a></div>
|
||||
</div>
|
||||
</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>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
@ -661,8 +661,124 @@
|
|||
</div>
|
||||
</x-content>
|
||||
</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>
|
||||
</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">
|
||||
<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%">
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
{
|
||||
"info": {
|
||||
"name": "Auto translated messages for language de",
|
||||
"name": "Auto translated messages for language de by the google translator",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Google Translate, via script by Markus Hadenfeldt",
|
||||
"email": "gtr.i18n.client@teaspeak.de"
|
||||
},
|
||||
{
|
||||
"name": "Markus Hadenfeldt",
|
||||
"email": "i18n.client@teaspeak.de"
|
||||
}
|
||||
]
|
||||
},
|
|
@ -3,6 +3,12 @@
|
|||
{
|
||||
"key": "de_DE",
|
||||
"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);
|
||||
};
|
||||
this.serverChat().name = tr("Server chat");
|
||||
|
||||
this.createChat("chat_channel", ChatType.CHANNEL).onMessageSend = (text: string) => {
|
||||
if(!globalClient.serverConnection) {
|
||||
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());
|
||||
};
|
||||
this.channelChat().name = tr("Channel chat");
|
||||
|
||||
globalClient.permissions.initializedListener.push(flag => {
|
||||
if(flag) this.activeChat0(this._activeChat);
|
||||
|
|
|
@ -11,6 +11,15 @@
|
|||
"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 {
|
||||
interface TranslationKey {
|
||||
message: string;
|
||||
|
@ -36,10 +45,26 @@ namespace i18n {
|
|||
}
|
||||
|
||||
interface TranslationFile {
|
||||
url: string;
|
||||
|
||||
info: FileInfo;
|
||||
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 fast_translate: { [key:string]:string; } = {};
|
||||
export function tr(message: string, key?: string) {
|
||||
|
@ -60,65 +85,238 @@ namespace i18n {
|
|||
return translated;
|
||||
}
|
||||
|
||||
export function load_file(url: string) : Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
async function load_translation_file(url: string) : Promise<TranslationFile> {
|
||||
return new Promise<TranslationFile>((resolve, reject) => {
|
||||
$.ajax({
|
||||
url: url,
|
||||
async: true,
|
||||
success: result => {
|
||||
console.dir(result);
|
||||
const file = (typeof(result) === "string" ? JSON.parse(result) : result) as TranslationFile;
|
||||
if(!file) {
|
||||
reject("Invalid json");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
console.dir(result);
|
||||
const file = (typeof(result) === "string" ? JSON.parse(result) : result) as TranslationFile;
|
||||
if(!file) {
|
||||
reject("Invalid json");
|
||||
return;
|
||||
}
|
||||
|
||||
//TODO validate file
|
||||
translations = file.translations;
|
||||
log.info(LogCategory.I18N, tr("Successfully initialized up translation file from %s"), url);
|
||||
resolve();
|
||||
file.url = url;
|
||||
//TODO validate file
|
||||
resolve(file);
|
||||
} 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) => {
|
||||
log.warn(LogCategory.I18N, "Failed to load translation file from \"%s\". Error: %o", url, error);
|
||||
reject("Failed to load file: " + error);
|
||||
reject(tr("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() {
|
||||
// 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 cfg = config.translation_config();
|
||||
|
||||
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;
|
||||
|
||||
/*
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
const tr: typeof i18n.tr = i18n.tr;
|
|
@ -92,7 +92,30 @@ function setup_jsrender() : boolean {
|
|||
}
|
||||
|
||||
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 {
|
||||
await i18n.initialize();
|
||||
} catch(error) {
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
/// <reference path="../../voice/AudioController.ts" />
|
||||
|
||||
namespace Modals {
|
||||
import info = log.info;
|
||||
import TranslationRepository = i18n.TranslationRepository;
|
||||
|
||||
export function spawnSettingsModal() {
|
||||
let modal;
|
||||
modal = createModal({
|
||||
|
@ -12,6 +15,7 @@ namespace Modals {
|
|||
let template = $("#tmpl_settings").renderTag();
|
||||
template = $.spawn("div").append(template);
|
||||
initialiseSettingListeners(modal,template = template.tabify());
|
||||
initialise_translations(template.find(".settings-translations"));
|
||||
return template;
|
||||
},
|
||||
footer: () => {
|
||||
|
@ -22,7 +26,7 @@ namespace Modals {
|
|||
footer.css("text-align", "right");
|
||||
|
||||
let buttonOk = $.spawn("button");
|
||||
buttonOk.text("Ok");
|
||||
buttonOk.text(tr("Ok"));
|
||||
buttonOk.click(() => modal.close());
|
||||
footer.append(buttonOk);
|
||||
|
||||
|
@ -270,4 +274,173 @@ namespace Modals {
|
|||
//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