Implementation of the translation system, inc generators.

canary
WolverinDEV 2018-12-09 20:18:49 +01:00
parent 2293302fbd
commit 27a92af825
24 changed files with 11690 additions and 371 deletions

2
.gitignore vendored
View File

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

View File

@ -1,5 +1,5 @@
import * as ts from "typescript";
import * as generator from "./generator";
import * as generator from "./ts_generator";
import {readFileSync} from "fs";
import * as glob from "glob";
@ -48,7 +48,6 @@ function compile(fileNames: string[], options: ts.CompilerOptions): void {
process.exit(exitCode);
}
console.log("Arguments: " + process.argv.slice(2));
const config = ts.parseCommandLine(process.argv.slice(2), file => readFileSync(file).toString());
console.dir(config);
if(config.errors && config.errors.length > 0) {

View File

@ -1,280 +1,3 @@
import * as ts from "typescript";
import * as sha256 from "sha256";
import {SyntaxKind} from "typescript";
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]));
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 TranslationEntry {
filename: string;
@ -282,25 +5,4 @@ export interface TranslationEntry {
character: number;
message: string;
}
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[];
}

View File

@ -1,9 +1,12 @@
import * as ts from "typescript";
import * as generator from "./generator";
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!");
@ -65,7 +68,7 @@ if(config.verbose) {
console.log("Input files:");
for(const file of config.source_files)
console.log(" - " + file);
console.log("Target file: " + config.target_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));
@ -94,9 +97,10 @@ function print(nodes: ts.Node[] | ts.SourceFile) : string {
);
}
const translations: TranslationEntry[] = [];
config.source_files.forEach(file => {
if(config.verbose)
console.log("iterating over %s", file)
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) {
@ -106,37 +110,52 @@ config.source_files.forEach(file => {
}
}
let source = ts.createSourceFile(
_file,
readFileSync(_file).toString(),
ts.ScriptTarget.ES2016,
true
);
console.log(print(source));
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);
console.log("Compile " + _file);
const messages = generator.generate(source, {
replace_cache: true
});
messages.forEach(message => {
console.log(message);
});
const messages = ts_generator.generate(source, {});
translations.push(...messages);
console.log("PRINT!");
console.log(print(source));
/*
result += "\n\n" + decl.print(decl.generate(source, {
remove_private: false
}));
*/
/*
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);
}
});
});
/*
mkdirp(path.normalize(path.dirname(base_path + "/" + target_file)), error => {
if(error)
throw error;
writeFileSync(base_path + "/" + target_file, result);
});
*/
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

@ -19,7 +19,7 @@ debugger;
debugger;
const zzz = true ? "yyy" : "bbb";
const y
const y = "";
debugger;
debugger;
debugger;

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>

297
build/trgen/ts_generator.ts Normal file
View File

@ -0,0 +1,297 @@
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[];
}

View File

@ -1,7 +1,11 @@
import * as ts from "typescript";
import * as generator from "./generator";
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;
@ -13,25 +17,30 @@ 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 || {};
if(process_config.verbose)
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 " + process_config.target_file);
console.log("Writing translation file to " + target);
writeFileSync(process_config.target_file, JSON.stringify(translations, null, 2));
mkdirp.sync(path.dirname(target));
writeFileSync(target, JSON.stringify(translations, null, 2));
}
});
return ctx => transformer(ctx);
}
const translations: generator.TranslationEntry[] = [];
const translations: TranslationEntry[] = [];
const transformer = (context: ts.TransformationContext) => (rootNode: ts.SourceFile) => {
console.log("Processing " + rootNode.fileName);
const result = generator.transform({
const result = ts_generator.transform({
use_window: false,
replace_cache: true
}, context, rootNode);

View File

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

View File

@ -8,7 +8,8 @@
"compile-sass": "sass --update .:.",
"build-worker": "tsc -p shared/js/workers/tsconfig_worker_codec.json",
"dtsgen": "node build/dtsgen/index.js",
"trgen": "node build/trgen/index.js"
"trgen": "node build/trgen/index.js",
"ttsc": "ttsc"
},
"author": "TeaSpeak (WolverinDEV)",
"license": "ISC",

View File

@ -0,0 +1,123 @@
"""
We want python 2.7 again...
"""
import json
"""
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 = {}
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)))
# TODO translate
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():
#print(run_translate(["Hello World", "Bla bla bla", "Im a unicorn"], "en", "de"))
translate_messages("generated/messages_script.json", "test.json", "de")
translate_messages("generated/messages_template.json", "test.json", "de")
pass
if __name__ == "__main__":
main()

17
shared/generate_translations.sh Executable file
View File

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

View File

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

View File

@ -990,8 +990,8 @@
<a>{{tr "Nickname"/}}</a>
<div class="help-tip tip-right tip-small" checked>
<p>
{{tr "Bans the client by his current nickname.<br>
The currently nickname cant be used until the ban expired"/}}
{{tr "Bans the client by his current nickname.<br>" +
"The currently nickname cant be used until the ban expired"/}}
</p>
</div>
</div>
@ -1000,11 +1000,11 @@
<a>{{tr "Hardware ID" /}}</a>
<div class="help-tip tip-right tip-small">
<p>
{{tr "Bans the client by his hardware id.<br>
The hardware id has different meanings, depends on the users agent<br>
TeaClient: The hardware id will be equal to the mac address<br>
TeaWeb: The TeaSpeak web client hasn't a hardware id, it will be random<br>
TeamSpeak 3 client: The hardware id will be a result of some hashes from hardware specific properties" /}}
{{tr "Bans the client by his hardware id.<br>" +
"The hardware id has different meanings, depends on the users agent<br>" +
"TeaClient: The hardware id will be equal to the mac address<br>" +
"TeaWeb: The TeaSpeak web client hasn't a hardware id, it will be random<br>" +
"TeamSpeak 3 client: The hardware id will be a result of some hashes from hardware specific properties" /}}
</p>
</div>
</div>
@ -1087,10 +1087,10 @@
<a>{{tr "Use this ban as a global ban" /}}</a>
<div class="help-tip tip-center tip-small">
<p>
{{tr "Global bans are bans which apply instance wide.<br>
This means that (if this rule apply to a victim) cant join <b>any</b> virtual server!<br>
Global bans are by default shown to every server admin group,<br>
but could only be created with query rights"/}}
{{tr "Global bans are bans which apply instance wide.<br>" +
"This means that (if this rule apply to a victim) cant join <b>any</b> virtual server!<br>" +
"Global bans are by default shown to every server admin group,<br>" +
"but could only be created with query rights"/}}
</p>
</div>
</div>

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": [ ]
}
]
}

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

@ -0,0 +1,8 @@
{
"translations": [
{
"key": "de_DE",
"path": "de_DE.translation"
}
]
}

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 */
]
}
]
}

5413
shared/i18n/test.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,124 @@
/*
"key": {
"message": "Show permission description",
"line": 374,
"character": 30,
"filename": "/home/wolverindev/TeaSpeak/TeaSpeak/Web-Client/shared/js/ui/modal/ModalPermissionEdit.ts"
},
"translated": "Berechtigungsbeschreibung anzeigen",
"flags": [
"google-translate",
"verified"
]
*/
namespace i18n {
interface TranslationKey {
message: string;
line?: number;
character?: number;
filename?: string;
}
interface Translation {
key: TranslationKey;
translated: string;
flags?: string[];
}
interface Contributor {
name: string;
email: string;
}
interface FileInfo {
name: string;
contributors: Contributor[];
}
interface TranslationFile {
info: FileInfo;
translations: Translation[];
}
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;
console.log("Translating \"%s\". Default: \"%s\"", key, message);
return message;
let translated = message;
for(const translation of translations) {
if(translation.key.message == message) {
translated = translation.translated;
break;
}
}
fast_translate[message] = translated;
return translated;
}
export function load_file(url: string) : Promise<void> {
return new Promise<void>((resolve, reject) => {
$.ajax({
url: url,
async: true,
success: result => {
console.dir(result);
const file = (typeof(result) === "string" ? JSON.parse(result) : result) as TranslationFile;
if(!file) {
reject("Invalid json");
return;
}
//TODO validate file
translations = file.translations;
log.info(LogCategory.I18N, tr("Successfully initialized up translation file from %s"), url);
resolve();
},
error: (xhr, error) => {
log.warn(LogCategory.I18N, "Failed to load translation file from \"%s\". Error: %o", url, error);
reject("Failed to load file: " + error);
}
})
});
}
export async function initialize() {
// await load_file("http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/web/environment/development/i18n/de_DE.translation");
await load_file("http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/web/environment/development/i18n/test.json");
}
}
const tr: typeof i18n.tr = i18n.tr;
const tr: typeof i18n.tr = i18n.tr;
/*
{
"info": {
"contributors": [
{
"name": "Markus Hadenfeldt",
"email": "i18n.client@teaspeak.de"
}
],
"name": "German translations"
},
"translations": [
{
"key": {
"message": "Show permission description",
"line": 374,
"character": 30,
"filename": "/home/wolverindev/TeaSpeak/TeaSpeak/Web-Client/shared/js/ui/modal/ModalPermissionEdit.ts"
},
"translated": "Berechtigungsbeschreibung anzeigen",
"flags": [
"google-translate",
"verified"
]
}
]
}
*/

View File

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

View File

@ -91,16 +91,34 @@ function setup_jsrender() : boolean {
return true;
}
function main() {
async function initialize() {
if(!setup_jsrender()) return;
//http://localhost:63343/Web-Client/index.php?_ijt=omcpmt8b9hnjlfguh8ajgrgolr&default_connect_url=true&default_connect_type=teamspeak&default_connect_url=localhost%3A9987&disableUnloadDialog=1&loader_ignore_age=1
AudioController.initializeAudioController();
if(!TSIdentityHelper.setup()) {
console.error(tr( "Could not setup the TeamSpeak identity parser!"));
try {
await i18n.initialize();
} catch(error) {
console.error(tr("Failed to initialized the translation system!\nError: %o"), error);
displayCriticalError("Failed to setup the translation system");
return;
}
AudioController.initializeAudioController();
if(!TSIdentityHelper.setup()) {
console.error(tr("Could not setup the TeamSpeak identity parser!"));
return;
}
try {
await ppt.initialize();
} catch(error) {
console.error(tr("Failed to initialize ppt!\nError: %o"), error);
displayCriticalError(tr("Failed to initialize ppt!"));
return;
}
}
function main() {
//http://localhost:63343/Web-Client/index.php?_ijt=omcpmt8b9hnjlfguh8ajgrgolr&default_connect_url=true&default_connect_type=teamspeak&default_connect_url=localhost%3A9987&disableUnloadDialog=1&loader_ignore_age=1
settings = new Settings();
globalClient = new TSClient();
/** Setup the XF forum identity **/
@ -150,11 +168,6 @@ function main() {
}
}
ppt.initialize().catch(error => {
console.error(tr("Failed to initialize ppt!"));
//TODO error notification?
});
/*
let tag = $("#tmpl_music_frame").renderTag({
//thumbnail: "img/loading_image.svg"
@ -183,8 +196,9 @@ function main() {
});
}
app.loadedListener.push(() => {
app.loadedListener.push(async () => {
try {
await initialize();
main();
if(!audio.player.initialized()) {
log.info(LogCategory.VOICE, tr("Initialize audio controller later!"));

5413
shared/test.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@
{
"transform": "../../build/trgen/ttsc_transformer.js",
"type": "program",
"target_file": "../generated/i18n.translation",
"target_file": "../generated/messages_script.json",
"verbose": true
}
]