Implementation of the translation system, inc generators.

This commit is contained in:
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/ node_modules/
auth/certs/ auth/certs/
auth/js/auth.js.map auth/js/auth.js.map
package-lock.json
.sass-cache/ .sass-cache/
.idea/ .idea/

View file

@ -1,5 +1,5 @@
import * as ts from "typescript"; import * as ts from "typescript";
import * as generator from "./generator"; import * as generator from "./ts_generator";
import {readFileSync} from "fs"; import {readFileSync} from "fs";
import * as glob from "glob"; import * as glob from "glob";
@ -48,7 +48,6 @@ function compile(fileNames: string[], options: ts.CompilerOptions): void {
process.exit(exitCode); process.exit(exitCode);
} }
console.log("Arguments: " + process.argv.slice(2));
const config = ts.parseCommandLine(process.argv.slice(2), file => readFileSync(file).toString()); const config = ts.parseCommandLine(process.argv.slice(2), file => readFileSync(file).toString());
console.dir(config); console.dir(config);
if(config.errors && config.errors.length > 0) { 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 { export interface TranslationEntry {
filename: string; filename: string;
@ -283,24 +6,3 @@ export interface TranslationEntry {
message: string; 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 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 {readFileSync, writeFileSync} from "fs";
import * as path from "path"; import * as path from "path";
import * as glob from "glob"; import * as glob from "glob";
import {isArray} from "util"; import {isArray} from "util";
import * as mkdirp from "mkdirp";
import {TranslationEntry} from "./generator";
console.log("TR GEN!"); console.log("TR GEN!");
@ -65,7 +68,7 @@ if(config.verbose) {
console.log("Input files:"); console.log("Input files:");
for(const file of config.source_files) for(const file of config.source_files)
console.log(" - " + file); 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)); 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 => { config.source_files.forEach(file => {
if(config.verbose) 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 => { glob.sync(config.base_bath + file).forEach(_file => {
_file = path.normalize(_file); _file = path.normalize(_file);
for(const n_file of negate_files) { for(const n_file of negate_files) {
@ -106,6 +110,8 @@ config.source_files.forEach(file => {
} }
} }
const file_type = path.extname(_file);
if(file_type == ".ts") {
let source = ts.createSourceFile( let source = ts.createSourceFile(
_file, _file,
readFileSync(_file).toString(), readFileSync(_file).toString(),
@ -116,27 +122,40 @@ config.source_files.forEach(file => {
console.log("Compile " + _file); console.log("Compile " + _file);
const messages = generator.generate(source, { const messages = ts_generator.generate(source, {});
replace_cache: true translations.push(...messages);
});
/*
messages.forEach(message => { messages.forEach(message => {
console.log(message); console.log(message);
}); });
console.log("PRINT!"); console.log("PRINT!");
console.log(print(source)); console.log(print(source));
*/
} else if(file_type == ".html") {
const messages = jsrender_generator.generate({}, {
content: readFileSync(_file).toString(),
name: _file
});
translations.push(...messages);
/* /*
result += "\n\n" + decl.print(decl.generate(source, { messages.forEach(message => {
remove_private: false 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(base_path + "/" + target_file)), error => { mkdirp(path.normalize(path.dirname(config.base_bath + config.target_file)), error => {
if(error) if(error)
throw error; throw error;
writeFileSync(base_path + "/" + target_file, result); 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; debugger;
const zzz = true ? "yyy" : "bbb"; const zzz = true ? "yyy" : "bbb";
const y const y = "";
debugger; debugger;
debugger; 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 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 {PluginConfig} from "ttypescript/lib/PluginCreator";
import {writeFileSync} from "fs"; import {writeFileSync} from "fs";
import {TranslationEntry} from "./generator";
interface Config { interface Config {
target_file?: string; 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 { export default function(program: ts.Program, config?: PluginConfig) : (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile {
process_config = config as any || {}; 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("TRGen transformer called");
console.log("Base path: %s", base_path);
}
process.on('exit', function () { 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.target_file) {
if(process_config.verbose) 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); return ctx => transformer(ctx);
} }
const translations: generator.TranslationEntry[] = []; const translations: TranslationEntry[] = [];
const transformer = (context: ts.TransformationContext) => (rootNode: ts.SourceFile) => { const transformer = (context: ts.TransformationContext) => (rootNode: ts.SourceFile) => {
console.log("Processing " + rootNode.fileName); console.log("Processing " + rootNode.fileName);
const result = generator.transform({ const result = ts_generator.transform({
use_window: false, use_window: false,
replace_cache: true replace_cache: true
}, context, rootNode); }, context, rootNode);

View file

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

View file

@ -8,7 +8,8 @@
"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 build/dtsgen/index.js",
"trgen": "node build/trgen/index.js" "trgen": "node build/trgen/index.js",
"ttsc": "ttsc"
}, },
"author": "TeaSpeak (WolverinDEV)", "author": "TeaSpeak (WolverinDEV)",
"license": "ISC", "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 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})
) )

View file

@ -990,8 +990,8 @@
<a>{{tr "Nickname"/}}</a> <a>{{tr "Nickname"/}}</a>
<div class="help-tip tip-right tip-small" checked> <div class="help-tip tip-right tip-small" checked>
<p> <p>
{{tr "Bans the client by his current nickname.<br> {{tr "Bans the client by his current nickname.<br>" +
The currently nickname cant be used until the ban expired"/}} "The currently nickname cant be used until the ban expired"/}}
</p> </p>
</div> </div>
</div> </div>
@ -1000,11 +1000,11 @@
<a>{{tr "Hardware ID" /}}</a> <a>{{tr "Hardware ID" /}}</a>
<div class="help-tip tip-right tip-small"> <div class="help-tip tip-right tip-small">
<p> <p>
{{tr "Bans the client by his hardware id.<br> {{tr "Bans the client by his hardware id.<br>" +
The hardware id has different meanings, depends on the users agent<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> "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> "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" /}} "TeamSpeak 3 client: The hardware id will be a result of some hashes from hardware specific properties" /}}
</p> </p>
</div> </div>
</div> </div>
@ -1087,10 +1087,10 @@
<a>{{tr "Use this ban as a global ban" /}}</a> <a>{{tr "Use this ban as a global ban" /}}</a>
<div class="help-tip tip-center tip-small"> <div class="help-tip tip-center tip-small">
<p> <p>
{{tr "Global bans are bans which apply instance wide.<br> {{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> "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> "Global bans are by default shown to every server admin group,<br>" +
but could only be created with query rights"/}} "but could only be created with query rights"/}}
</p> </p>
</div> </div>
</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 { 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) { export function tr(message: string, key?: string) {
const sloppy = fast_translate[message];
if(sloppy) return sloppy;
console.log("Translating \"%s\". Default: \"%s\"", key, message); 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, PERMISSIONS,
GENERAL, GENERAL,
NETWORKING, NETWORKING,
VOICE VOICE,
I18N
} }
namespace log { namespace log {
@ -17,7 +18,6 @@ namespace log {
ERROR ERROR
} }
//TODO add translation
let category_mapping = new Map<number, string>([ let category_mapping = new Map<number, string>([
[LogCategory.CHANNEL, "Channel "], [LogCategory.CHANNEL, "Channel "],
[LogCategory.CLIENT, "Client "], [LogCategory.CLIENT, "Client "],
@ -25,7 +25,8 @@ namespace log {
[LogCategory.PERMISSIONS, "Permission "], [LogCategory.PERMISSIONS, "Permission "],
[LogCategory.GENERAL, "General "], [LogCategory.GENERAL, "General "],
[LogCategory.NETWORKING, "Network "], [LogCategory.NETWORKING, "Network "],
[LogCategory.VOICE, "Voice "] [LogCategory.VOICE, "Voice "],
[LogCategory.I18N, "I18N "]
]); ]);
function logDirect(type: LogType, message: string, ...optionalParams: any[]) { function logDirect(type: LogType, message: string, ...optionalParams: any[]) {

View file

@ -91,16 +91,34 @@ function setup_jsrender() : boolean {
return true; return true;
} }
function main() { async function initialize() {
if(!setup_jsrender()) return; if(!setup_jsrender()) return;
try {
//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 await i18n.initialize();
AudioController.initializeAudioController(); } catch(error) {
if(!TSIdentityHelper.setup()) { console.error(tr("Failed to initialized the translation system!\nError: %o"), error);
console.error(tr( "Could not setup the TeamSpeak identity parser!")); displayCriticalError("Failed to setup the translation system");
return; 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 **/
@ -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({ let tag = $("#tmpl_music_frame").renderTag({
//thumbnail: "img/loading_image.svg" //thumbnail: "img/loading_image.svg"
@ -183,8 +196,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!"));

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", "transform": "../../build/trgen/ttsc_transformer.js",
"type": "program", "type": "program",
"target_file": "../generated/i18n.translation", "target_file": "../generated/messages_script.json",
"verbose": true "verbose": true
} }
] ]