Merged translation system (#17)

* A lots of translation changes (Generate translation files)

* Removed auto generated package lock file

* Implementation of the translation system, inc generators.

* improved loader error handling

* 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?

* Finalizing the translation system

* Finalized translation system and added polish and turkish google translation

* Finally done :)

* Fixed defautl repositories dosnt show up

* fixed settings not initialized
canary
WolverinDEV 2018-12-15 14:04:29 +01:00 committed by GitHub
parent 3ec5db38d8
commit bbc929d46d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 25943 additions and 4616 deletions

2
.gitignore vendored
View File

@ -9,7 +9,7 @@ generated/
node_modules/
auth/certs/
auth/js/auth.js.map
package-lock.json
.sass-cache/
.idea/

View File

@ -75,6 +75,14 @@
"path" => "wasm/",
"local-path" => "./asm/generated/"
],
[ /* translations */
"type" => "i18n",
"search-pattern" => "/.*\.(translation|json)/",
"build-target" => "dev|rel",
"path" => "i18n/",
"local-path" => "./shared/i18n/"
],
/* vendors */
[

0
out.d.ts vendored Normal file
View File

4253
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,9 @@
"scripts": {
"compile-sass": "sass --update .:.",
"build-worker": "tsc -p shared/js/workers/tsconfig_worker_codec.json",
"dtsgen": "node build/dtsgen/index.js"
"dtsgen": "node tools/dtsgen/index.js",
"trgen": "node tools/trgen/index.js",
"ttsc": "ttsc"
},
"author": "TeaSpeak (WolverinDEV)",
"license": "ISC",
@ -17,10 +19,13 @@
"@types/jquery": "3.3.5",
"@types/moment": "^2.13.0",
"@types/node": "^9.4.6",
"@types/sha256": "^0.2.0",
"@types/websocket": "0.0.38",
"electron": "^3.0.2",
"gulp": "^3.9.1",
"sass": "^1.14.1",
"sha256": "^0.2.0",
"ttypescript": "^1.5.5",
"typescript": "^3.1.1"
},
"repository": {

View File

@ -20,7 +20,7 @@ BASEDIR=$(dirname "$0")
cd "$BASEDIR/../"
#Building the generator
cd build/dtsgen
cd tools/dtsgen
execute_tsc -p tsconfig.json
if [ $? -ne 0 ]; then
echo "Failed to build typescript declaration generator"

View File

@ -1,26 +1,31 @@
#!/usr/bin/env bash
function execute_tsc() {
if [ "$command_tsc" == "" ]; then
if [ "$node_bin" == "" ]; then
node_bin=$(npm bin)
fi
execute_npm_command tsc $@
}
if [ ! -e "${node_bin}/tsc" ]; then
echo "Could not find tsc command"
function execute_ttsc() {
execute_npm_command ttsc $@
}
function execute_npm_command() {
command_name=$1
command_variable="command_$command_name"
#echo "Variable names $command_variable"
if [ "${!command_variable}" == "" ]; then
node_bin=$(npm bin)
#echo "Node root ${node_bin}"
if [ ! -e "${node_bin}/${command_name}" ]; then
echo "Could not find \"$command_name\" command"
echo "May type npm install"
exit 1
fi
command_tsc="${node_bin}/tsc"
output=$(${command_tsc} -v)
if [ $? -ne 0 ]; then
echo "Failed to execute a simple tsc command!"
echo "$output"
exit 1
fi
eval "${command_variable}=\"${node_bin}/${command_name}\""
fi
${command_tsc} $@
echo "Arguments: ${@:2}"
${!command_variable} ${@:2}
}

View File

@ -60,14 +60,14 @@ if [ "$type" == "release" ]; then #Compile everything for release mode
fi
elif [ "$type" == "development" ]; then
echo "Building shared source"
execute_tsc -p ./shared/tsconfig/tsconfig.json
execute_ttsc -p ./shared/tsconfig/tsconfig.json
if [ $? -ne 0 ]; then
echo "Failed to compile shared sources"
exit 1
fi
echo "Building web client source"
execute_tsc -p ./web/tsconfig/tsconfig.json
execute_ttsc -p ./web/tsconfig/tsconfig.json
if [ $? -ne 0 ]; then
echo "Failed to compile web sources"
exit 1

View File

@ -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;
}
}
}
}

View File

@ -0,0 +1,134 @@
#!/usr/bin/env python2.7
"""
We want python 2.7 again...
"""
import json
import sys
"""
from googletrans import Translator # Use the free webhook
def run_translate(messages, source_language, target_language):
translator = Translator()
_translations = translator.translate(messages, src=source_language, dest=target_language)
result = []
for translation in _translations:
result.append({
"source": translation.origin,
"translated": translation.text
})
return result
"""
from google.cloud import translate # Use googles could solution
def run_translate(messages, source_language, target_language):
translate_client = translate.Client()
# The text to translate
text = u'Hello, world!'
# The target language
result = []
limit = 16
for chunk in [messages[i:i + limit] for i in xrange(0, len(messages), limit)]:
# Translates some text into Russian
print("Requesting {} translations".format(len(chunk)))
translations = translate_client.translate(chunk, target_language=target_language)
for translation in translations:
result.append({
"source": translation["input"],
"translated": translation["translatedText"]
})
return result
def translate_messages(source, destination, target_language):
with open(source) as f:
data = json.load(f)
result = {
"translations": [],
"info": None
}
try:
with open(destination) as f:
result = json.load(f)
print("loaded old result")
except:
pass
translations = result["translations"]
if translations is None:
print("Using new translation map")
translations = []
else:
print("Loaded {} old translations".format(len(translations)))
messages = []
for message in data:
try:
messages.index(message["message"])
except:
try:
found = False
for entry in translations:
if entry["key"]["message"] == message["message"]:
found = True
break
if not found:
raise Exception('add message for translate')
except:
messages.append(message["message"])
print("Translating {} messages".format(len(messages)))
if len(messages) != 0:
_translations = run_translate(messages, 'en', target_language)
print("Messages translated, generating target file")
for translation in _translations:
translations.append({
"key": {
"message": translation["source"]
},
"translated": translation["translated"],
"flags": [
"google-translate"
]
})
print("Writing target file")
result["translations"] = translations
if result["info"] is None:
result["info"] = {
"contributors": [
{
"name": "Google Translate, via script by Markus Hadenfeldt",
"email": "gtr.i18n.client@teaspeak.de"
}
],
"name": "Auto translated messages for language " + target_language
}
with open(destination, 'w') as f:
f.write(json.dumps(result, indent=2))
print("Done")
def main(target_language):
target_file = "i18n/{}_google_translate.translation".format(target_language)
translate_messages("generated/messages_script.json", target_file, target_language)
translate_messages("generated/messages_template.json", target_file, target_language)
pass
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Invalid argument count!")
print("Usage: ./generate_i18n_gtranslate.py <language>")
exit(1)
main(sys.argv[1])

View File

@ -19,7 +19,7 @@ if [ ! -e ${LOADER_FILE} ]; then
exit 1
fi
execute_tsc -p tsconfig/tsconfig_packed.json
execute_ttsc -p tsconfig/tsconfig_packed.json
if [ $? -ne 0 ]; then
echo "Failed to generate packed file!"
exit 1

17
shared/generate_translations.sh Executable file
View File

@ -0,0 +1,17 @@
#!/usr/bin/env bash
BASEDIR=$(dirname "$0")
cd "$BASEDIR"
#Generate the script translations
npm run ttsc -- -p $(pwd)/tsconfig/tsconfig.json
if [ $? -ne 0 ]; then
echo "Failed to generate translation file for the script files"
exit 1
fi
npm run trgen -- -f $(pwd)/html/templates.html -d $(pwd)/generated/messages_template.json
if [ $? -ne 0 ]; then
echo "Failed to generate translations file for the template files"
exit 1
fi

View File

@ -1,6 +1,7 @@
"""
This should be executed as python 2.7 (because of pydub)
This should be executed with python 2.7 (because of pydub)
"""
import os
import requests
import json
@ -25,7 +26,7 @@ def tts(text, file):
'Chrome/69.0.3497.100 Safari/537.36 OPR/56.0.3051.52',
'content-type': 'application/x-www-form-urlencoded',
'referer': 'https://www.naturalreaders.com/online/',
'authority': 'kfiuqykx63.execute-api.us-east-1.amazonaws.com'
'authority': 'kfiuqykx63.execute-api.us-east-1.amazonaws.com' #You may need to change that here
},
data=json.dumps({"t": text})
)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
{
"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"
]
},
{
"key": {
"message": "Create a new connection"
},
"translated": "Verbinden",
"flags": [ ]
}
]
}

File diff suppressed because it is too large Load Diff

18
shared/i18n/info.json Normal file
View File

@ -0,0 +1,18 @@
{
"translations": [
{
"key": "de_gt",
"path": "de_google_translate.translation"
},
{
"key": "pl_gt",
"path": "pl_google_translate.translation"
},
{
"key": "tr_gt",
"path": "tr_google_translate.translation"
}
],
"name": "Default TeaSpeak repository",
"contact": "i18n@teaspeak.de"
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
{
"info": {
"contributors": [
/* add yourself if you have done anything :) */
{
"name": "Markus Hadenfeldt", /* this field is required */
"email": "i18n.client@teaspeak.de" /* this field is required */
}
],
"name": "A template translation file" /* this field is required */
},
"translations": [ /* Array with all translation objects */
{ /* translation object */
"key": { /* the key */
"message": "Show permission description", /* necessary to identify the message */
"line": 374, /* optional, only for specify the translation for a specific case (Not supported yet!) */
"character": 30, /* optional, only for specify the translation for a specific case (Not supported yet!) */
"filename": "/home/wolverindev/TeaSpeak/TeaSpeak/Web-Client/shared/js/ui/modal/ModalPermissionEdit.ts" /* optional, only for specify the translation for a specific case (Not supported yet!) */
},
"translated": "Berechtigungsbeschreibung anzeigen", /* The actual translation */
"flags": [ /* some flags for this translation */
"google-translate", /* this translation has been made with google translator */
"verified" /* this translation has been verified by a native speaker */
]
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -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);

View File

@ -98,7 +98,7 @@ class TSClient {
helpers.hashPassword(password.password).then(password => {
this.serverConnection.startConnection({host, port}, new HandshakeHandler(identity, name, password));
}).catch(error => {
createErrorModal(tr("Error while hashing password"), tr("Failed to hash server password!<br>" + error).open();
createErrorModal(tr("Error while hashing password"), tr("Failed to hash server password!<br>") + error).open();
})
} else
this.serverConnection.startConnection({host, port}, new HandshakeHandler(identity, name, password ? password.password : undefined));

View File

@ -1,8 +1,331 @@
namespace i18n {
export function tr(message: string, key?: string) {
console.log("Translating \"%s\". Default: \"%s\"", key, message);
/*
"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"
]
*/
function guid() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
}
return message;
namespace i18n {
export interface TranslationKey {
message: string;
line?: number;
character?: number;
filename?: string;
}
export interface Translation {
key: TranslationKey;
translated: string;
flags?: string[];
}
export interface Contributor {
name: string;
email: string;
}
export interface FileInfo {
name: string;
contributors: Contributor[];
}
export 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) {
const sloppy = fast_translate[message];
if(sloppy) return sloppy;
log.info(LogCategory.I18N, "Translating \"%s\". Default: \"%s\"", key, message);
let translated = message;
for(const translation of translations) {
if(translation.key.message == message) {
translated = translation.translated;
break;
}
}
fast_translate[message] = translated;
return translated;
}
async function load_translation_file(url: string) : Promise<TranslationFile> {
return new Promise<TranslationFile>((resolve, reject) => {
$.ajax({
url: url,
async: true,
success: result => {
try {
const file = (typeof(result) === "string" ? JSON.parse(result) : result) as TranslationFile;
if(!file) {
reject("Invalid json");
return;
}
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) => {
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;
if(config.repositories.length == 0) {
//Add the default TeaSpeak repository
load_repository(StaticSettings.instance.static("i18n.default_repository", "i18n/")).then(repo => {
log.info(LogCategory.I18N, tr("Successfully added default repository from \"%s\"."), repo.url);
register_repository(repo);
}).catch(error => {
log.warn(LogCategory.I18N, tr("Failed to add default repository. Error: %o"), error);
});
}
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 = () => {
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() {
const rcfg = config.repository_config(); /* initialize */
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;

View File

@ -1,5 +1,3 @@
//FIXME fix display critical error before load
namespace app {
export enum Type {
UNDEFINED,
@ -55,30 +53,40 @@ namespace app {
}
/* define that here */
let impl_display_critical_error: (message: string) => any;
let _critical_triggered = false;
const display_critical_load = message => {
if(_critical_triggered) return; /* only show the first error */
_critical_triggered = true;
let tag = document.getElementById("critical-load");
let detail = tag.getElementsByClassName("detail")[0];
detail.innerHTML = message;
tag.style.display = "block";
fadeoutLoader();
};
const loader_impl_display_critical_error = message => {
if(typeof(createErrorModal) !== 'undefined') {
createErrorModal("A critical error occurred while loading the page!", message, {closeable: false}).open();
} else {
display_critical_load(message);
}
fadeoutLoader();
};
interface Window {
impl_display_critical_error: (_: string) => any;
}
if(!window.impl_display_critical_error) { /* default impl */
impl_display_critical_error = message => {
if(typeof(createErrorModal) !== 'undefined') {
createErrorModal("A critical error occurred while loading the page!", message, {closeable: false}).open();
} else {
let tag = document.getElementById("critical-load");
let detail = tag.getElementsByClassName("detail")[0];
detail.innerHTML = message;
tag.style.display = "block";
}
fadeoutLoader();
}
if(!window.impl_display_critical_error) { /* default impl */
window.impl_display_critical_error = loader_impl_display_critical_error;
}
function displayCriticalError(message: string) {
if(window.impl_display_critical_error)
window.impl_display_critical_error(message);
else
console.error("Could not display a critical message: " + message); /* this shall never happen! */
loader_impl_display_critical_error(message);
}
@ -280,14 +288,7 @@ function loadTemplates() {
}
while(tags.length > 0){
let tag = tags.item(0);
if(tag.id == "tmpl_main") {
let main_node = document.createElement("div");
document.getElementsByTagName("body").item(0).appendChild(main_node);
main_node.outerHTML = tag.innerHTML;
tag.remove();
}
else
root.appendChild(tag);
root.appendChild(tag);
}
}).catch(error => {
@ -344,11 +345,22 @@ function loadSide() {
load_script("js/proto.js").then(loadDebug).catch(loadRelease);
//Load the teaweb templates
loadTemplates();
}).catch(error => {
displayCriticalError("Failed to load scripts.<br>Lookup the console for more details.");
console.error(error);
});
}
//FUN: loader_ignore_age=0&loader_default_duration=1500&loader_default_age=5000
let _fadeout_warned = false;
function fadeoutLoader(duration = undefined, minAge = undefined, ignoreAge = undefined) {
if(typeof($) === "undefined") {
if(!_fadeout_warned)
console.warn("Could not fadeout loader screen. Missing jquery functions.");
_fadeout_warned = true;
return;
}
let settingsDefined = typeof(StaticSettings) !== "undefined";
if(!duration) {
if(settingsDefined)
@ -371,6 +383,7 @@ function fadeoutLoader(duration = undefined, minAge = undefined, ignoreAge = und
setTimeout(() => fadeoutLoader(duration, 0, true), minAge - age);
return;
}
$(".loader .bookshelf_wrapper").animate({top: 0, opacity: 0}, duration);
$(".loader .half").animate({width: 0}, duration, () => {
$(".loader").detach();
@ -410,4 +423,9 @@ navigator.browserSpecs = (function(){
})();
console.log(navigator.browserSpecs); //Object { name: "Firefox", version: "42" }
loadSide();
try {
loadSide();
} catch(error) {
displayCriticalError("Failed to invoke main loader function.");
console.error(error);
}

View File

@ -5,7 +5,8 @@ enum LogCategory {
PERMISSIONS,
GENERAL,
NETWORKING,
VOICE
VOICE,
I18N
}
namespace log {
@ -17,7 +18,6 @@ namespace log {
ERROR
}
//TODO add translation
let category_mapping = new Map<number, string>([
[LogCategory.CHANNEL, "Channel "],
[LogCategory.CLIENT, "Client "],
@ -25,7 +25,8 @@ namespace log {
[LogCategory.PERMISSIONS, "Permission "],
[LogCategory.GENERAL, "General "],
[LogCategory.NETWORKING, "Network "],
[LogCategory.VOICE, "Voice "]
[LogCategory.VOICE, "Voice "],
[LogCategory.I18N, "I18N "]
]);
function logDirect(type: LogType, message: string, ...optionalParams: any[]) {

View File

@ -78,6 +78,10 @@ function setup_jsrender() : boolean {
return moment(arguments[0]).format(arguments[1]);
});
js_render.views.tags("tr", (...arguments) => {
return tr(arguments[0]);
});
$(".jsrender-template").each((idx, _entry) => {
if(!js_render.templates(_entry.id, _entry.innerHTML)) { //, _entry.innerHTML
console.error("Failed to cache template " + _entry.id + " for js render!");
@ -87,16 +91,57 @@ function setup_jsrender() : boolean {
return true;
}
function main() {
if(!setup_jsrender()) return;
async function initialize() {
const display_load_error = message => {
if(typeof(display_critical_load) !== "undefined")
display_critical_load(message);
else
displayCriticalError(message);
};
//http://localhost:63343/Web-Client/index.php?_ijt=omcpmt8b9hnjlfguh8ajgrgolr&default_connect_url=true&default_connect_type=teamspeak&default_connect_url=localhost%3A9987&disableUnloadDialog=1&loader_ignore_age=1
AudioController.initializeAudioController();
if(!TSIdentityHelper.setup()) {
console.error(tr( "Could not setup the TeamSpeak identity parser!"));
try {
await i18n.initialize();
} catch(error) {
console.error(tr("Failed to initialized the translation system!\nError: %o"), error);
displayCriticalError("Failed to setup the translation system");
return;
}
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;
}
AudioController.initializeAudioController();
if(!TSIdentityHelper.setup()) {
console.error(tr("Could not setup the TeamSpeak identity parser!"));
return;
}
try {
await ppt.initialize();
} catch(error) {
console.error(tr("Failed to initialize ppt!\nError: %o"), error);
displayCriticalError(tr("Failed to initialize ppt!"));
return;
}
}
function main() {
//http://localhost:63343/Web-Client/index.php?_ijt=omcpmt8b9hnjlfguh8ajgrgolr&default_connect_url=true&default_connect_type=teamspeak&default_connect_url=localhost%3A9987&disableUnloadDialog=1&loader_ignore_age=1
settings = new Settings();
globalClient = new TSClient();
/** Setup the XF forum identity **/
@ -146,11 +191,6 @@ function main() {
}
}
ppt.initialize().catch(error => {
console.error(tr("Failed to initialize ppt!"));
//TODO error notification?
});
/*
let tag = $("#tmpl_music_frame").renderTag({
//thumbnail: "img/loading_image.svg"
@ -179,8 +219,9 @@ function main() {
});
}
app.loadedListener.push(() => {
app.loadedListener.push(async () => {
try {
await initialize();
main();
if(!audio.player.initialized()) {
log.info(LogCategory.VOICE, tr("Initialize audio controller later!"));

View File

@ -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();
}
}
}

View File

@ -1,3 +1,4 @@
/// <reference path="../i18n/localize.ts" />
interface JQuery<TElement = HTMLElement> {
asTabWidget(copy?: boolean) : JQuery<TElement>;

5413
shared/test.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,15 @@
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"sourceMap": true
"sourceMap": true,
"plugins": [ /* ttypescript */
{
"transform": "../../tools/trgen/ttsc_transformer.js",
"type": "program",
"target_file": "../generated/messages_script.json",
"verbose": true
}
]
},
"exclude": [
"../js/workers"

View File

@ -6,7 +6,15 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "none",
"outFile": "../generated/shared.js"
"outFile": "../generated/shared.js",
"plugins": [ /* ttypescript */
{
"transform": "../../tools/trgen/ttsc_transformer.js",
"type": "program",
"target_file": "../generated/messages_script.json",
"verbose": true
}
]
},
"exclude": [
"../js/workers",

View File

@ -252,7 +252,13 @@ generators[SyntaxKind.Constructor] = (settings, stack, node: ts.ConstructorDecla
generators[SyntaxKind.FunctionDeclaration] = (settings, stack, node: ts.FunctionDeclaration) => {
if(stack.flag_namespace && !has_modifier(node.modifiers, SyntaxKind.ExportKeyword)) return;
return ts.createFunctionDeclaration(node.decorators, append_declare(node.modifiers, !stack.flag_declare), node.asteriskToken, node.name, node.typeParameters, _generate_param_declare(settings, stack, node.parameters), node.type, undefined);
let return_type = node.type;
if(has_modifier(node.modifiers, SyntaxKind.AsyncKeyword)) {
if(!return_type)
return_type = ts.createTypeReferenceNode("Promise", [ts.createIdentifier("any") as any]);
}
return ts.createFunctionDeclaration(node.decorators, remove_modifier(append_declare(node.modifiers, !stack.flag_declare), SyntaxKind.AsyncKeyword), node.asteriskToken, node.name, node.typeParameters, _generate_param_declare(settings, stack, node.parameters), return_type, undefined);
};

View File

@ -31,6 +31,10 @@ while(args.length > 0) {
config_file = args[1];
base_path = path.normalize(path.dirname(config_file));
args = args.slice(2);
} else if(args[0] == "-b" || args[0] == "--base") {
base_path = args[1];
base_path = path.normalize(base_path);
args = args.slice(2);
} else {
console.error("Invalid command line option %s", args[0]);
process.exit(1);

View File

@ -31,4 +31,17 @@ namespace T {
export function Y() {}
}
}
namespace T {
export async function async_void() {}
export async function async_any() : Promise<any> {
return "" as any;
}
export async function async_number() : Promise<number> {
return 0;
}
export async function async_number_string() : Promise<number | string> {
return 0;
}
}

6
tools/trgen/bin/tsc.sh Executable file
View File

@ -0,0 +1,6 @@
#!/bin/bash
BASEDIR=$(dirname "$0")
FILE="${BASEDIR}/../compiler.ts"
npm run dtsgen -- $@

84
tools/trgen/compiler.ts Normal file
View File

@ -0,0 +1,84 @@
import * as ts from "typescript";
import * as generator from "./ts_generator";
import {readFileSync} from "fs";
import * as glob from "glob";
import * as path from "path";
const transformer = <T extends ts.Node>(context: ts.TransformationContext) => (rootNode: T) => {
return generator.transform({
use_window: false,
replace_cache: true,
verbose: true
}, context, rootNode as any).node;
};
function compile(fileNames: string[], options: ts.CompilerOptions): void {
const program: ts.Program = ts.createProgram(fileNames, options);
//(context: TransformationContext) => Transformer<T>;
let emitResult = program.emit(undefined, undefined, undefined, undefined, {
before: [ transformer ]
});
let allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics);
allDiagnostics.forEach(diagnostic => {
if (diagnostic.file) {
let { line, character } = diagnostic.file.getLineAndCharacterOfPosition(
diagnostic.start!
);
let message = ts.flattenDiagnosticMessageText(
diagnostic.messageText,
"\n"
);
console.log(
`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`
);
} else {
console.log(
`${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`
);
}
});
let exitCode = emitResult.emitSkipped ? 1 : 0;
console.log(`Process exiting with code '${exitCode}'.`);
process.exit(exitCode);
}
const config = ts.parseCommandLine(process.argv.slice(2), file => readFileSync(file).toString());
console.dir(config);
if(config.errors && config.errors.length > 0) {
for(const error of config.errors)
console.log(error.messageText);
process.exit(1);
}
if(config.options.project) {
const project = ts.readConfigFile(config.options.project, file => readFileSync(file).toString()).config;
const base_path = path.dirname(config.options.project) + "/";
console.dir(project);
const negate_files: string[] = [].concat.apply([], (project.exclude || []).map(file => glob.sync(base_path + file))).map(file => path.normalize(file));
project.include.forEach(file => {
glob.sync(base_path + file).forEach(_file => {
_file = path.normalize(_file);
for(const n_file of negate_files) {
if(n_file == _file) {
console.log("Skipping %s", _file);
return;
}
}
config.fileNames.push(_file);
});
});
Object.assign(config.options, project.compilerOptions);
console.log(config.options);
}
compile(config.fileNames, config.options);

8
tools/trgen/generator.ts Normal file
View File

@ -0,0 +1,8 @@
export interface TranslationEntry {
filename: string;
line: number;
character: number;
message: string;
}

161
tools/trgen/index.ts Normal file
View File

@ -0,0 +1,161 @@
import * as ts from "typescript";
import * as ts_generator from "./ts_generator";
import * as jsrender_generator from "./jsrender_generator";
import {readFileSync, writeFileSync} from "fs";
import * as path from "path";
import * as glob from "glob";
import {isArray} from "util";
import * as mkdirp from "mkdirp";
import {TranslationEntry} from "./generator";
console.log("TR GEN!");
/*
const files = ["/home/wolverindev/TeaSpeak/TeaSpeak/Web-Client/build/trgen/test/test_01.ts"];
files.forEach(file => {
let source = ts.createSourceFile(
file,
readFileSync(file).toString(),
ts.ScriptTarget.ES2016,
true
);
generator.generate(source);
});
*/
interface Config {
source_files?: string[];
excluded_files?: string[];
target_file?: string;
verbose?: boolean;
base_bath?: string;
file_config?: string;
}
let config: Config = {};
let args = process.argv.slice(2);
while(args.length > 0) {
if(args[0] == "-f" || args[0] == "--file") {
(config.source_files || (config.source_files = [])).push(args[1]);
args = args.slice(2);
} else if(args[0] == "-e" || args[0] == "--exclude") {
(config.excluded_files || (config.excluded_files = [])).push(args[1]);
args = args.slice(2);
} else if(args[0] == "-d" || args[0] == "--destination") {
config.target_file = args[1];
args = args.slice(2);
} else if(args[0] == "-v" || args[0] == "--verbose") {
config.verbose = true;
args = args.slice(1);
} else if(args[0] == "-c" || args[0] == "--config") {
config.file_config = args[1];
config.base_bath = path.normalize(path.dirname(config.file_config)) + "/";
args = args.slice(2);
} else {
console.error("Invalid command line option \"%s\"", args[0]);
process.exit(1);
}
}
config.base_bath = config.base_bath || "";
if(config.verbose) {
console.log("Base path: " + config.base_bath);
console.log("Input files:");
for(const file of config.source_files)
console.log(" - " + file);
console.log("Target file: " + config.target_file);
}
const negate_files: string[] = [].concat.apply([], (config.excluded_files || []).map(file => glob.sync(config.base_bath + file))).map(file => path.normalize(file));
let result = "";
function print(nodes: ts.Node[] | ts.SourceFile) : string {
if(!isArray(nodes) && nodes.kind == ts.SyntaxKind.SourceFile)
nodes = (<ts.SourceFile>nodes).getChildren();
const dummy_file = ts.createSourceFile(
"dummy_file",
"",
ts.ScriptTarget.ES2016,
false,
ts.ScriptKind.TS
);
const printer = ts.createPrinter({
newLine: ts.NewLineKind.LineFeed
});
return printer.printList(
ts.ListFormat.SpaceBetweenBraces | ts.ListFormat.MultiLine | ts.ListFormat.PreferNewLine,
nodes as any,
dummy_file
);
}
const translations: TranslationEntry[] = [];
config.source_files.forEach(file => {
if(config.verbose)
console.log("iterating over %s (%s)", file, path.resolve(path.normalize(config.base_bath + file)));
glob.sync(config.base_bath + file).forEach(_file => {
_file = path.normalize(_file);
for(const n_file of negate_files) {
if(n_file == _file) {
console.log("Skipping %s", _file);
return;
}
}
const file_type = path.extname(_file);
if(file_type == ".ts") {
let source = ts.createSourceFile(
_file,
readFileSync(_file).toString(),
ts.ScriptTarget.ES2016,
true
);
console.log(print(source));
console.log("Compile " + _file);
const messages = ts_generator.generate(source, {});
translations.push(...messages);
/*
messages.forEach(message => {
console.log(message);
});
console.log("PRINT!");
console.log(print(source));
*/
} else if(file_type == ".html") {
const messages = jsrender_generator.generate({}, {
content: readFileSync(_file).toString(),
name: _file
});
translations.push(...messages);
/*
messages.forEach(message => {
console.log(message);
});
*/
} else {
console.log("Unknown file type \"%s\". Skipping file %s", file_type, _file);
}
});
});
if(config.target_file) {
mkdirp(path.normalize(path.dirname(config.base_bath + config.target_file)), error => {
if(error)
throw error;
writeFileSync(config.base_bath + config.target_file, JSON.stringify(translations, null, 2));
});
} else {
console.log(JSON.stringify(translations, null, 2));
}

View File

@ -0,0 +1,53 @@
import {TranslationEntry} from "./generator";
export interface Configuration {
}
export interface File {
content: string;
name: string;
}
/* Well my IDE hates me and does not support groups. By default with ES6 groups are supported... nvm */
//const regex = /{{ *tr *(?<message_expression>(("([^"]|\\")*")|('([^']|\\')*')|(`([^`]|\\`)+`)|( *\+ *)?)+) *\/ *}}/;
const regex = /{{ *tr *((("([^"]|\\")*")|('([^']|\\')*')|(`([^`]|\\`)+`)|([\n ]*\+[\n ]*)?)+) *\/ *}}/;
export function generate(config: Configuration, file: File) : TranslationEntry[] {
let result: TranslationEntry[] = [];
const lines = file.content.split('\n');
let match: RegExpExecArray;
let base_index = 0;
while(match = regex.exec(file.content.substr(base_index))) {
let expression = ((<any>match).groups || {})["message_expression"] || match[1];
//expression = expression.replace(/\n/g, "\\n");
let message;
try {
message = eval(expression);
} catch (error) {
console.error("Failed to evaluate expression:\n%s", expression);
base_index += match.index + match[0].length;
continue;
}
let character = base_index + match.index;
let line;
for(line = 0; line < lines.length; line++) {
const length = lines[line].length + 1;
if(length > character) break;
character -= length;
}
result.push({
filename: file.name,
character: character + 1,
line: line + 1,
message: message
});
base_index += match.index + match[0].length;
}
return result;
}

View File

@ -0,0 +1,27 @@
function tr(message: string) : string {
console.log("Message: " + message);
return message;
}
const x = tr("yyy");
function y() {
const y = tr(tr("yyy"));
}
console.log("XXX: " + tr("XXX"));
console.log("YYY: " + tr("YYY"));
var z = 1 + 2 + 3;
debugger;
debugger;
debugger;
debugger;
const zzz = true ? "yyy" : "bbb";
const y = "";
debugger;
debugger;
debugger;
debugger;
const { a } = {a : ""};

View File

@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script class="jsrender-template" id="tmpl_connect" type="text/html">
<div style="margin-top: 5px;">
<div style="display: flex; flex-direction: row; width: 100%; justify-content: space-between">
<div style="width: 68%; margin-bottom: 5px">
<div>{{tr "Remote address and port:" /}}</div>
<input type="text" style="width: 100%" class="connect_address" value="unknown">
</div>
<div style="width: 20%">
<div>{{tr "Server password:" /}}</div>
<form name="server_password_form" onsubmit="return false;">
<input type="password" id="connect_server_password_{{rnd '0~13377331'/}}" autocomplete="off" style="width: 100%" class="connect_password">
</form>
</div>
</div>
<div>
<div>{{tr "Nickname:" /}}</div>
<input type="text" style="width: 100%" class="connect_nickname" value="">
</div>
<hr>
<div class="group_box">
<div style="display: flex; justify-content: space-between;">
<div style="text-align: right;">{{tr "Identity Settings" /}}</div>
<select class="identity_select">
<option name="identity_type" value="TEAFORO">{{tr "Forum Account" /}}</option>
<option name="identity_type" value="TEAMSPEAK">{{tr "TeamSpeak" /}}</option>
<option name="identity_type" value="NICKNAME">{{tr "Nickname (Debug purposes only!)" /}}</option> <!-- Only available on localhost for debug -->
</select>
</div>
<hr>
<div class="identity_config_TEAMSPEAK identity_config">
{{tr "Please enter your exported TS3 Identity string bellow or select your exported Identity"/}}<br>
<div style="width: 100%; display: flex; justify-content: stretch; flex-direction: row">
<input placeholder="Identity string" style="min-width: 60%; flex-shrink: 1; flex-grow: 2; margin: 5px;" class="identity_string">
<div style="max-width: 200px; flex-grow: 1; flex-shrink: 4; margin: 5px"><input style="display: flex; width: 100%;" class="identity_file" type="file"></div>
</div>
</div>
<div class="identity_config_TEAFORO identity_config">
<div class="connected">
{{tr "You're using your forum account as verification"/}}
</div>
<div class="disconnected">
<!-- TODO tr -->
You cant use your TeaSpeak forum account.<br>
You're not connected!<br>
Click {{if !client}}<a href="{{:forum_path}}login.php">here</a>{{else}}<a href="#" class="native-teaforo-login">here</a>{{/if}} to login via the TeaSpeak forum.
</div>
</div>
<div class="identity_config_NICKNAME identity_config">
{{tr "This is just for debug and uses the name as unique identifier" /}}
{{tr "This is just for debug and uses the name as unique identifier" + "Hello World :D" /}}
{{tr "This is just for debug and uses the name as unique identifier" + `Hello World :D 2` /}}
</div>
<div style="background-color: red; border-radius: 1px; display: none" class="error_message"></div>
</div> <!-- <a href="<?php echo authPath() . "login.php"; ?>">Login</a> via the TeaSpeak forum. -->
</div>
</script>
</body>
</html>

296
tools/trgen/ts_generator.ts Normal file
View File

@ -0,0 +1,296 @@
import * as ts from "typescript";
import * as sha256 from "sha256";
import {SyntaxKind} from "typescript";
import {TranslationEntry} from "./generator";
export function generate(file: ts.SourceFile, config: Configuration) : TranslationEntry[] {
let result: TranslationEntry[] = [];
file.forEachChild(n => _generate(config, n, result));
return result;
}
function report(node: ts.Node, message: string) {
const sf = node.getSourceFile();
let { line, character } = sf ? sf.getLineAndCharacterOfPosition(node.getStart()) : {line: -1, character: -1};
console.log(`${(sf || {fileName: "unknown"}).fileName} (${line + 1},${character + 1}): ${message}`);
}
function _generate(config: Configuration, node: ts.Node, result: TranslationEntry[]) {
//console.log("Node: %s", SyntaxKind[node.kind]);
call_analize:
if(ts.isCallExpression(node)) {
const call = <ts.CallExpression>node;
const call_name = call.expression["escapedText"] as string;
if(call_name != "tr") break call_analize;
console.dir(call_name);
console.log("Parameters: %o", call.arguments.length);
if(call.arguments.length > 1) {
report(call, "Invalid argument count");
node.forEachChild(n => _generate(config, n, result));
return;
}
const object = <ts.StringLiteral>call.arguments[0];
if(object.kind != SyntaxKind.StringLiteral) {
report(call, "Invalid argument: " + SyntaxKind[object.kind]);
node.forEachChild(n => _generate(config, n, result));
return;
}
console.log("Message: %o", object.text);
//FIXME
if(config.replace_cache) {
console.log("Update!");
ts.updateCall(call, call.expression, call.typeArguments, [ts.createLiteral("PENIS!")]);
}
const { line, character } = node.getSourceFile().getLineAndCharacterOfPosition(node.getStart());
result.push({
filename: node.getSourceFile().fileName,
line: line,
character: character,
message: object.text
});
}
node.forEachChild(n => _generate(config, n, result));
}
function create_unique_check(source_file: ts.SourceFile, variable: ts.Expression, variables: { name: string, node: ts.Node }[]) : ts.Node[] {
const nodes: ts.Node[] = [], blocked_nodes: ts.Statement[] = [];
const node_path = (node: ts.Node) => {
const sf = node.getSourceFile();
let { line, character } = sf ? sf.getLineAndCharacterOfPosition(node.getStart()) : {line: -1, character: -1};
return `${(sf || {fileName: "unknown"}).fileName} (${line + 1},${character + 1})`;
};
const create_error = (variable_name: ts.Expression, variable_path: ts.Expression, other_path: ts.Expression) => {
return [
ts.createLiteral("Translation with generated name \""),
variable_name,
ts.createLiteral("\" already exists!\nIt has been already defined here: "),
other_path,
ts.createLiteral("\nAttempted to redefine here: "),
variable_path,
ts.createLiteral("\nRegenerate and/or fix your program!")
].reduce((a, b) => ts.createBinary(a, SyntaxKind.PlusToken, b));
};
let declarations_file: ts.Expression;
const unique_check_label_name = "unique_translation_check";
/* initialization */
{
const declarations = ts.createElementAccess(variable, ts.createLiteral("declared"));
nodes.push(ts.createAssignment(declarations, ts.createBinary(declarations, SyntaxKind.BarBarToken, ts.createAssignment(declarations, ts.createObjectLiteral()))));
declarations_file = ts.createElementAccess(variable, ts.createLiteral("declared_files"));
nodes.push(ts.createAssignment(declarations_file, ts.createBinary(declarations_file, SyntaxKind.BarBarToken, ts.createAssignment(declarations_file, ts.createObjectLiteral()))));
variable = declarations;
}
/* test file already loaded */
{
const unique_id = sha256(source_file.fileName + " | " + (Date.now() / 1000));
const property = ts.createElementAccess(declarations_file, ts.createLiteral(unique_id));
const if_condition = ts.createBinary(property, SyntaxKind.ExclamationEqualsEqualsToken, ts.createIdentifier("undefined"));
//
let if_then: ts.Block;
{
const elements: ts.Statement[] = [];
const console = ts.createIdentifier("console.warn");
elements.push(ts.createCall(console, [], [ts.createLiteral("This file has already been loaded!\nAre you executing scripts twice?") as any]) as any);
elements.push(ts.createBreak(unique_check_label_name));
if_then = ts.createBlock(elements);
}
const if_else = ts.createAssignment(property, ts.createLiteral(unique_id));
blocked_nodes.push(ts.createIf(if_condition, if_then, if_else as any));
}
/* test if variable has been defined somewhere else */
{
const for_variable_name = ts.createLoopVariable();
const for_variable_path = ts.createLoopVariable();
const for_declaration = ts.createVariableDeclarationList([ts.createVariableDeclaration(ts.createObjectBindingPattern([
ts.createBindingElement(undefined, "name", for_variable_name, undefined),
ts.createBindingElement(undefined, "path", for_variable_path, undefined)])
, undefined, undefined)]);
let for_block: ts.Statement;
{ //Create the for block
const elements: ts.Statement[] = [];
const property = ts.createElementAccess(variable, for_variable_name);
const if_condition = ts.createBinary(property, SyntaxKind.ExclamationEqualsEqualsToken, ts.createIdentifier("undefined"));
//
const if_then = ts.createThrow(create_error(for_variable_name, for_variable_path, property));
const if_else = ts.createAssignment(property, for_variable_path);
const if_valid = ts.createIf(if_condition, if_then, if_else as any);
elements.push(if_valid);
for_block = ts.createBlock(elements);
}
let block = ts.createForOf(undefined,
for_declaration, ts.createArrayLiteral(
[...variables.map(e => ts.createObjectLiteral([
ts.createPropertyAssignment("name", ts.createLiteral(e.name)),
ts.createPropertyAssignment("path", ts.createLiteral(node_path(e.node)))
]))
])
, for_block);
block = ts.addSyntheticLeadingComment(block, SyntaxKind.MultiLineCommentTrivia, "Auto generated helper for testing if the translation keys are unique", true);
blocked_nodes.push(block);
}
return [...nodes, ts.createLabel(unique_check_label_name, ts.createBlock(blocked_nodes))];
}
export function transform(config: Configuration, context: ts.TransformationContext, node: ts.SourceFile) : TransformResult {
const cache: VolatileTransformConfig = {} as any;
cache.translations = [];
//Initialize nodes
const extra_nodes: ts.Node[] = [];
{
cache.nodes = {} as any;
if(config.use_window) {
const window = ts.createIdentifier("window");
let translation_map = ts.createPropertyAccess(window, ts.createIdentifier("_translations"));
const new_translations = ts.createAssignment(translation_map, ts.createObjectLiteral());
let translation_map_init: ts.Expression = ts.createBinary(translation_map, ts.SyntaxKind.BarBarToken, new_translations);
translation_map_init = ts.createParen(translation_map_init);
cache.nodes = {
translation_map: translation_map,
translation_map_init: translation_map_init
};
} else {
const variable_name = "_translations";
const variable_map = ts.createIdentifier(variable_name);
const inline_if = ts.createBinary(ts.createBinary(ts.createTypeOf(variable_map), SyntaxKind.ExclamationEqualsEqualsToken, ts.createLiteral("undefined")), ts.SyntaxKind.BarBarToken, ts.createAssignment(variable_map, ts.createObjectLiteral()));
cache.nodes = {
translation_map: variable_map,
translation_map_init: variable_map
};
//ts.createVariableDeclarationList([ts.createVariableDeclaration(variable_name)], ts.NodeFlags.Let)
extra_nodes.push(inline_if);
}
}
const generated_names: { name: string, node: ts.Node }[] = [];
cache.name_generator = (config, node, message) => {
const characters = "0123456789_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
let name = "";
while(name.length < 8) {
const char = characters[Math.floor(Math.random() * characters.length)];
name = name + char;
if(name[0] >= '0' && name[0] <= '9')
name = name.substr(1) || "";
}
//FIXME
//if(generated_names.indexOf(name) != -1)
// return cache.name_generator(config, node, message);
generated_names.push({name: name, node: node});
return name;
};
function visit(node: ts.Node): ts.Node {
node = ts.visitEachChild(node, visit, context);
return replace_processor(config, cache, node);
}
node = ts.visitNode(node, visit);
extra_nodes.push(...create_unique_check(node, cache.nodes.translation_map_init, generated_names));
node = ts.updateSourceFileNode(node, [...(extra_nodes as any[]), ...node.statements], node.isDeclarationFile, node.referencedFiles, node.typeReferenceDirectives, node.hasNoDefaultLib, node.referencedFiles);
const result: TransformResult = {} as any;
result.node = node;
result.translations = cache.translations;
return result;
}
export function replace_processor(config: Configuration, cache: VolatileTransformConfig, node: ts.Node) : ts.Node {
if(config.verbose)
console.log("Process %s", SyntaxKind[node.kind]);
if(ts.isCallExpression(node)) {
const call = <ts.CallExpression>node;
const call_name = call.expression["escapedText"] as string;
if(call_name != "tr") return node;
if(config.verbose) {
console.dir(call_name);
console.log("Parameters: %o", call.arguments.length);
}
if(call.arguments.length > 1) {
report(call, "Invalid argument count");
return node;
}
const object = <ts.StringLiteral>call.arguments[0];
if(object.kind != SyntaxKind.StringLiteral) {
report(call, "Invalid argument: " + SyntaxKind[object.kind]);
return node;
}
if(config.verbose)
console.log("Message: %o", object.text || object.getText());
const variable_name = ts.createIdentifier(cache.name_generator(config, node, object.text || object.getText()));
const variable_init = ts.createPropertyAccess(cache.nodes.translation_map_init, variable_name);
const variable = ts.createPropertyAccess(cache.nodes.translation_map, variable_name);
const new_variable = ts.createAssignment(variable, call);
const source_file = node.getSourceFile();
let { line, character } = source_file ? source_file.getLineAndCharacterOfPosition(node.getStart()) : {line: -1, character: -1};
cache.translations.push({
message: object.text || object.getText(),
line: line,
character: character,
filename: (source_file || {fileName: "unknown"}).fileName
});
return ts.createBinary(variable_init, ts.SyntaxKind.BarBarToken, new_variable);
}
return node;
}
export interface Configuration {
use_window?: boolean;
replace_cache?: boolean;
verbose?: boolean;
}
export interface TransformResult {
node: ts.SourceFile;
translations: TranslationEntry[];
}
interface VolatileTransformConfig {
nodes: {
translation_map: ts.Expression;
translation_map_init: ts.Expression;
};
name_generator: (config: Configuration, node: ts.Node, message: string) => string;
translations: TranslationEntry[];
}

15
tools/trgen/tsconfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"baseUrl": ".",
"moduleResolution": "node",
"module": "commonjs",
"lib": ["es6"],
"typeRoots": [],
"types": []
},
"files": [
"generator.ts",
"index.ts"
]
}

View File

@ -0,0 +1,67 @@
import * as ts from "typescript";
import * as ts_generator from "./ts_generator";
import * as path from "path";
import * as mkdirp from "mkdirp";
import {PluginConfig} from "ttypescript/lib/PluginCreator";
import {writeFileSync} from "fs";
import {TranslationEntry} from "./generator";
interface Config {
target_file?: string;
verbose?: boolean;
}
//(program: ts.Program, config?: PluginConfig) => ts.TransformerFactory
let process_config: Config;
export default function(program: ts.Program, config?: PluginConfig) : (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile {
process_config = config as any || {};
const base_path = path.dirname(program.getCompilerOptions().project || program.getCurrentDirectory());
if(process_config.verbose) {
console.log("TRGen transformer called");
console.log("Base path: %s", base_path);
}
process.on('exit', function () {
const target = path.isAbsolute(process_config.target_file) ? process_config.target_file : path.join(base_path, process_config.target_file);
if(process_config.target_file) {
if(process_config.verbose)
console.log("Writing translation file to " + target);
mkdirp.sync(path.dirname(target));
writeFileSync(target, JSON.stringify(translations, null, 2));
}
});
return ctx => transformer(ctx) as any;
}
const translations: TranslationEntry[] = [];
const transformer = (context: ts.TransformationContext) =>
(rootNode: ts.Node) => {
const handler = (rootNode: ts.Node) => {
if(rootNode.kind == ts.SyntaxKind.Bundle) {
const bundle = rootNode as ts.Bundle;
const result = [];
for(const file of bundle.sourceFiles)
result.push(handler(file));
return ts.updateBundle(bundle, result as any, bundle.prepends as any);
} else if(rootNode.kind == ts.SyntaxKind.SourceFile) {
const file = rootNode as ts.SourceFile;
console.log("Processing " + file.fileName);
const result = ts_generator.transform({
use_window: false,
replace_cache: true
}, context, file);
translations.push(...result.translations);
return result.node;
} else {
console.warn("Invalid transform input: %s", ts.SyntaxKind[rootNode.kind]);
}
};
return handler(rootNode);
};

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Translation Manager</title>
</head>
<body>
<div>This needs some improvements</div>
</body>
</html>

2
vendor/bbcode vendored

@ -1 +1 @@
Subproject commit fafda400bc654848531f8aa163b6cac7cc7abebe
Subproject commit 7b931ed61cf265937dc742579f9070e7c4e50775