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/ node_modules/
auth/certs/ auth/certs/
auth/js/auth.js.map auth/js/auth.js.map
package-lock.json
.sass-cache/ .sass-cache/
.idea/ .idea/

View File

@ -75,6 +75,14 @@
"path" => "wasm/", "path" => "wasm/",
"local-path" => "./asm/generated/" "local-path" => "./asm/generated/"
], ],
[ /* translations */
"type" => "i18n",
"search-pattern" => "/.*\.(translation|json)/",
"build-target" => "dev|rel",
"path" => "i18n/",
"local-path" => "./shared/i18n/"
],
/* vendors */ /* 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": { "scripts": {
"compile-sass": "sass --update .:.", "compile-sass": "sass --update .:.",
"build-worker": "tsc -p shared/js/workers/tsconfig_worker_codec.json", "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)", "author": "TeaSpeak (WolverinDEV)",
"license": "ISC", "license": "ISC",
@ -17,10 +19,13 @@
"@types/jquery": "3.3.5", "@types/jquery": "3.3.5",
"@types/moment": "^2.13.0", "@types/moment": "^2.13.0",
"@types/node": "^9.4.6", "@types/node": "^9.4.6",
"@types/sha256": "^0.2.0",
"@types/websocket": "0.0.38", "@types/websocket": "0.0.38",
"electron": "^3.0.2", "electron": "^3.0.2",
"gulp": "^3.9.1", "gulp": "^3.9.1",
"sass": "^1.14.1", "sass": "^1.14.1",
"sha256": "^0.2.0",
"ttypescript": "^1.5.5",
"typescript": "^3.1.1" "typescript": "^3.1.1"
}, },
"repository": { "repository": {

View File

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

View File

@ -1,26 +1,31 @@
#!/usr/bin/env bash #!/usr/bin/env bash
function execute_tsc() { function execute_tsc() {
if [ "$command_tsc" == "" ]; then execute_npm_command tsc $@
if [ "$node_bin" == "" ]; then }
node_bin=$(npm bin)
fi
if [ ! -e "${node_bin}/tsc" ]; then function execute_ttsc() {
echo "Could not find tsc command" 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" echo "May type npm install"
exit 1 exit 1
fi fi
command_tsc="${node_bin}/tsc" eval "${command_variable}=\"${node_bin}/${command_name}\""
output=$(${command_tsc} -v)
if [ $? -ne 0 ]; then
echo "Failed to execute a simple tsc command!"
echo "$output"
exit 1
fi
fi 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 fi
elif [ "$type" == "development" ]; then elif [ "$type" == "development" ]; then
echo "Building shared source" echo "Building shared source"
execute_tsc -p ./shared/tsconfig/tsconfig.json execute_ttsc -p ./shared/tsconfig/tsconfig.json
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "Failed to compile shared sources" echo "Failed to compile shared sources"
exit 1 exit 1
fi fi
echo "Building web client source" echo "Building web client source"
execute_tsc -p ./web/tsconfig/tsconfig.json execute_ttsc -p ./web/tsconfig/tsconfig.json
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "Failed to compile web sources" echo "Failed to compile web sources"
exit 1 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 exit 1
fi fi
execute_tsc -p tsconfig/tsconfig_packed.json execute_ttsc -p tsconfig/tsconfig_packed.json
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "Failed to generate packed file!" echo "Failed to generate packed file!"
exit 1 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 os
import requests import requests
import json import json
@ -25,7 +26,7 @@ def tts(text, file):
'Chrome/69.0.3497.100 Safari/537.36 OPR/56.0.3051.52', 'Chrome/69.0.3497.100 Safari/537.36 OPR/56.0.3051.52',
'content-type': 'application/x-www-form-urlencoded', 'content-type': 'application/x-www-form-urlencoded',
'referer': 'https://www.naturalreaders.com/online/', '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}) 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); globalClient.serverConnection.sendMessage(text, ChatType.SERVER);
}; };
this.serverChat().name = tr("Server chat");
this.createChat("chat_channel", ChatType.CHANNEL).onMessageSend = (text: string) => { this.createChat("chat_channel", ChatType.CHANNEL).onMessageSend = (text: string) => {
if(!globalClient.serverConnection) { if(!globalClient.serverConnection) {
chat.channelChat().appendError(tr("Could not send chant message (Not connected)")); chat.channelChat().appendError(tr("Could not send chant message (Not connected)"));
@ -331,6 +333,7 @@ class ChatBox {
globalClient.serverConnection.sendMessage(text, ChatType.CHANNEL, globalClient.getClient().currentChannel()); globalClient.serverConnection.sendMessage(text, ChatType.CHANNEL, globalClient.getClient().currentChannel());
}; };
this.channelChat().name = tr("Channel chat");
globalClient.permissions.initializedListener.push(flag => { globalClient.permissions.initializedListener.push(flag => {
if(flag) this.activeChat0(this._activeChat); if(flag) this.activeChat0(this._activeChat);

View File

@ -98,7 +98,7 @@ class TSClient {
helpers.hashPassword(password.password).then(password => { helpers.hashPassword(password.password).then(password => {
this.serverConnection.startConnection({host, port}, new HandshakeHandler(identity, name, password)); this.serverConnection.startConnection({host, port}, new HandshakeHandler(identity, name, password));
}).catch(error => { }).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 } else
this.serverConnection.startConnection({host, port}, new HandshakeHandler(identity, name, password ? password.password : undefined)); 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) { "key": {
console.log("Translating \"%s\". Default: \"%s\"", key, message); "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; const tr: typeof i18n.tr = i18n.tr;

View File

@ -1,5 +1,3 @@
//FIXME fix display critical error before load
namespace app { namespace app {
export enum Type { export enum Type {
UNDEFINED, UNDEFINED,
@ -55,30 +53,40 @@ namespace app {
} }
/* define that here */ /* 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 { interface Window {
impl_display_critical_error: (_: string) => any; 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"; if(!window.impl_display_critical_error) { /* default impl */
} window.impl_display_critical_error = loader_impl_display_critical_error;
fadeoutLoader();
}
} }
function displayCriticalError(message: string) { function displayCriticalError(message: string) {
if(window.impl_display_critical_error) if(window.impl_display_critical_error)
window.impl_display_critical_error(message); window.impl_display_critical_error(message);
else 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){ while(tags.length > 0){
let tag = tags.item(0); let tag = tags.item(0);
if(tag.id == "tmpl_main") { root.appendChild(tag);
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);
} }
}).catch(error => { }).catch(error => {
@ -344,11 +345,22 @@ function loadSide() {
load_script("js/proto.js").then(loadDebug).catch(loadRelease); load_script("js/proto.js").then(loadDebug).catch(loadRelease);
//Load the teaweb templates //Load the teaweb templates
loadTemplates(); 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 //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) { 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"; let settingsDefined = typeof(StaticSettings) !== "undefined";
if(!duration) { if(!duration) {
if(settingsDefined) if(settingsDefined)
@ -371,6 +383,7 @@ function fadeoutLoader(duration = undefined, minAge = undefined, ignoreAge = und
setTimeout(() => fadeoutLoader(duration, 0, true), minAge - age); setTimeout(() => fadeoutLoader(duration, 0, true), minAge - age);
return; return;
} }
$(".loader .bookshelf_wrapper").animate({top: 0, opacity: 0}, duration); $(".loader .bookshelf_wrapper").animate({top: 0, opacity: 0}, duration);
$(".loader .half").animate({width: 0}, duration, () => { $(".loader .half").animate({width: 0}, duration, () => {
$(".loader").detach(); $(".loader").detach();
@ -410,4 +423,9 @@ navigator.browserSpecs = (function(){
})(); })();
console.log(navigator.browserSpecs); //Object { name: "Firefox", version: "42" } 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, PERMISSIONS,
GENERAL, GENERAL,
NETWORKING, NETWORKING,
VOICE VOICE,
I18N
} }
namespace log { namespace log {
@ -17,7 +18,6 @@ namespace log {
ERROR ERROR
} }
//TODO add translation
let category_mapping = new Map<number, string>([ let category_mapping = new Map<number, string>([
[LogCategory.CHANNEL, "Channel "], [LogCategory.CHANNEL, "Channel "],
[LogCategory.CLIENT, "Client "], [LogCategory.CLIENT, "Client "],
@ -25,7 +25,8 @@ namespace log {
[LogCategory.PERMISSIONS, "Permission "], [LogCategory.PERMISSIONS, "Permission "],
[LogCategory.GENERAL, "General "], [LogCategory.GENERAL, "General "],
[LogCategory.NETWORKING, "Network "], [LogCategory.NETWORKING, "Network "],
[LogCategory.VOICE, "Voice "] [LogCategory.VOICE, "Voice "],
[LogCategory.I18N, "I18N "]
]); ]);
function logDirect(type: LogType, message: string, ...optionalParams: any[]) { 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]); return moment(arguments[0]).format(arguments[1]);
}); });
js_render.views.tags("tr", (...arguments) => {
return tr(arguments[0]);
});
$(".jsrender-template").each((idx, _entry) => { $(".jsrender-template").each((idx, _entry) => {
if(!js_render.templates(_entry.id, _entry.innerHTML)) { //, _entry.innerHTML if(!js_render.templates(_entry.id, _entry.innerHTML)) { //, _entry.innerHTML
console.error("Failed to cache template " + _entry.id + " for js render!"); console.error("Failed to cache template " + _entry.id + " for js render!");
@ -87,16 +91,57 @@ function setup_jsrender() : boolean {
return true; return true;
} }
function main() { 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);
};
//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 try {
AudioController.initializeAudioController(); await i18n.initialize();
if(!TSIdentityHelper.setup()) { } catch(error) {
console.error(tr( "Could not setup the TeamSpeak identity parser!")); console.error(tr("Failed to initialized the translation system!\nError: %o"), error);
displayCriticalError("Failed to setup the translation system");
return; 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(); settings = new Settings();
globalClient = new TSClient(); globalClient = new TSClient();
/** Setup the XF forum identity **/ /** 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({ let tag = $("#tmpl_music_frame").renderTag({
//thumbnail: "img/loading_image.svg" //thumbnail: "img/loading_image.svg"
@ -179,8 +219,9 @@ function main() {
}); });
} }
app.loadedListener.push(() => { app.loadedListener.push(async () => {
try { try {
await initialize();
main(); main();
if(!audio.player.initialized()) { if(!audio.player.initialized()) {
log.info(LogCategory.VOICE, tr("Initialize audio controller later!")); log.info(LogCategory.VOICE, tr("Initialize audio controller later!"));

View File

@ -4,6 +4,9 @@
/// <reference path="../../voice/AudioController.ts" /> /// <reference path="../../voice/AudioController.ts" />
namespace Modals { namespace Modals {
import info = log.info;
import TranslationRepository = i18n.TranslationRepository;
export function spawnSettingsModal() { export function spawnSettingsModal() {
let modal; let modal;
modal = createModal({ modal = createModal({
@ -12,6 +15,7 @@ namespace Modals {
let template = $("#tmpl_settings").renderTag(); let template = $("#tmpl_settings").renderTag();
template = $.spawn("div").append(template); template = $.spawn("div").append(template);
initialiseSettingListeners(modal,template = template.tabify()); initialiseSettingListeners(modal,template = template.tabify());
initialise_translations(template.find(".settings-translations"));
return template; return template;
}, },
footer: () => { footer: () => {
@ -22,7 +26,7 @@ namespace Modals {
footer.css("text-align", "right"); footer.css("text-align", "right");
let buttonOk = $.spawn("button"); let buttonOk = $.spawn("button");
buttonOk.text("Ok"); buttonOk.text(tr("Ok"));
buttonOk.click(() => modal.close()); buttonOk.click(() => modal.close());
footer.append(buttonOk); footer.append(buttonOk);
@ -270,4 +274,173 @@ namespace Modals {
//Initialise speakers //Initialise speakers
} }
function initialise_translations(tag: JQuery) {
{ //Initialize the list
const tag_list = tag.find(".setting-list .list");
const tag_loading = tag.find(".setting-list .loading");
const template = $("#settings-translations-list-entry");
const restart_hint = tag.find(".setting-list .restart-note");
restart_hint.hide();
const update_list = () => {
tag_list.empty();
const currently_selected = i18n.config.translation_config().current_translation_url;
{ //Default translation
const tag = template.renderTag({
type: "default",
selected: !currently_selected || currently_selected == "default"
});
tag.on('click', () => {
i18n.select_translation(undefined, undefined);
tag_list.find(".selected").removeClass("selected");
tag.addClass("selected");
restart_hint.show();
});
tag.appendTo(tag_list);
}
{
const display_repository_info = (repository: TranslationRepository) => {
const info_modal = createModal({
header: tr("Repository info"),
body: () => {
return $("#settings-translations-list-entry-info").renderTag({
type: "repository",
name: repository.name,
url: repository.url,
contact: repository.contact,
translations: repository.translations || []
});
},
footer: () => {
let footer = $.spawn("div");
footer.addClass("modal-button-group");
footer.css("margin-top", "5px");
footer.css("margin-bottom", "5px");
footer.css("text-align", "right");
let buttonOk = $.spawn("button");
buttonOk.text(tr("Close"));
buttonOk.click(() => info_modal.close());
footer.append(buttonOk);
return footer;
}
});
info_modal.open()
};
tag_loading.show();
i18n.iterate_translations((repo, entry) => {
let repo_tag = tag_list.find("[repository=\"" + repo.unique_id + "\"]");
if(repo_tag.length == 0) {
repo_tag = template.renderTag({
type: "repository",
name: repo.name || repo.url,
id: repo.unique_id
});
repo_tag.find(".button-delete").on('click', e => {
e.preventDefault();
Modals.spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this repository?"), answer => {
if(answer) {
i18n.delete_repository(repo);
update_list();
}
});
});
repo_tag.find(".button-info").on('click', e => {
e.preventDefault();
display_repository_info(repo);
});
tag_list.append(repo_tag);
}
const tag = template.renderTag({
type: "translation",
name: entry.info.name || entry.url,
id: repo.unique_id,
selected: i18n.config.translation_config().current_translation_url == entry.url
});
tag.find(".button-info").on('click', e => {
e.preventDefault();
const info_modal = createModal({
header: tr("Translation info"),
body: () => {
const tag = $("#settings-translations-list-entry-info").renderTag({
type: "translation",
name: entry.info.name,
url: entry.url,
repository_name: repo.name,
contributors: entry.info.contributors || []
});
tag.find(".button-info").on('click', () => display_repository_info(repo));
return tag;
},
footer: () => {
let footer = $.spawn("div");
footer.addClass("modal-button-group");
footer.css("margin-top", "5px");
footer.css("margin-bottom", "5px");
footer.css("text-align", "right");
let buttonOk = $.spawn("button");
buttonOk.text(tr("Close"));
buttonOk.click(() => info_modal.close());
footer.append(buttonOk);
return footer;
}
});
info_modal.open()
});
tag.on('click', e => {
if(e.isDefaultPrevented()) return;
i18n.select_translation(repo, entry);
tag_list.find(".selected").removeClass("selected");
tag.addClass("selected");
restart_hint.show();
});
tag.insertAfter(repo_tag)
}, () => {
tag_loading.hide();
});
}
};
{
tag.find(".button-add-repository").on('click', () => {
createInputModal("Enter URL", tr("Enter repository URL:<br>"), text => true, url => { //FIXME test valid url
if(!url) return;
tag_loading.show();
i18n.load_repository(url as string).then(repository => {
i18n.register_repository(repository);
update_list();
}).catch(error => {
tag_loading.hide();
createErrorModal("Failed to load repository", tr("Failed to query repository.<br>Ensure that this repository is valid and reachable.<br>Error: ") + error).open();
})
}).open();
});
}
restart_hint.find(".button-reload").on('click', () => {
location.reload();
});
update_list();
}
}
} }

View File

@ -1,3 +1,4 @@
/// <reference path="../i18n/localize.ts" />
interface JQuery<TElement = HTMLElement> { interface JQuery<TElement = HTMLElement> {
asTabWidget(copy?: boolean) : JQuery<TElement>; 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": { "compilerOptions": {
"target": "es6", "target": "es6",
"module": "commonjs", "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": [ "exclude": [
"../js/workers" "../js/workers"

View File

@ -6,7 +6,15 @@
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"module": "none", "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": [ "exclude": [
"../js/workers", "../js/workers",

View File

@ -252,7 +252,13 @@ generators[SyntaxKind.Constructor] = (settings, stack, node: ts.ConstructorDecla
generators[SyntaxKind.FunctionDeclaration] = (settings, stack, node: ts.FunctionDeclaration) => { generators[SyntaxKind.FunctionDeclaration] = (settings, stack, node: ts.FunctionDeclaration) => {
if(stack.flag_namespace && !has_modifier(node.modifiers, SyntaxKind.ExportKeyword)) return; 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]; config_file = args[1];
base_path = path.normalize(path.dirname(config_file)); base_path = path.normalize(path.dirname(config_file));
args = args.slice(2); 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 { } else {
console.error("Invalid command line option %s", args[0]); console.error("Invalid command line option %s", args[0]);
process.exit(1); process.exit(1);

View File

@ -31,4 +31,17 @@ namespace T {
export function Y() {} 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