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 initializedcanary
parent
3ec5db38d8
commit
bbc929d46d
|
@ -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/
|
||||||
|
|
|
@ -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 */
|
||||||
[
|
[
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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": {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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])
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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);
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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;
|
|
@ -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);
|
||||||
|
}
|
|
@ -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[]) {
|
||||||
|
|
|
@ -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!"));
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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>;
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
BASEDIR=$(dirname "$0")
|
||||||
|
FILE="${BASEDIR}/../compiler.ts"
|
||||||
|
|
||||||
|
npm run dtsgen -- $@
|
|
@ -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);
|
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
export interface TranslationEntry {
|
||||||
|
filename: string;
|
||||||
|
line: number;
|
||||||
|
character: number;
|
||||||
|
|
||||||
|
message: string;
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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 : ""};
|
|
@ -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>
|
|
@ -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[];
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["es6"],
|
||||||
|
|
||||||
|
"typeRoots": [],
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"generator.ts",
|
||||||
|
"index.ts"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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);
|
||||||
|
};
|
|
@ -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>
|
|
@ -1 +1 @@
|
||||||
Subproject commit fafda400bc654848531f8aa163b6cac7cc7abebe
|
Subproject commit 7b931ed61cf265937dc742579f9070e7c4e50775
|
Loading…
Reference in New Issue