2018-12-15 14:04:29 +01:00
import * as ts from "typescript" ;
import { SyntaxKind } from "typescript" ;
2020-08-01 14:54:44 +02:00
import * as sha256 from "sha256" ;
2018-12-15 14:04:29 +01:00
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 ;
}
2020-03-31 15:19:53 +02:00
const source_location = ( node : ts.Node ) = > {
2018-12-15 14:04:29 +01:00
const sf = node . getSourceFile ( ) ;
let { line , character } = sf ? sf . getLineAndCharacterOfPosition ( node . getStart ( ) ) : { line : - 1 , character : - 1 } ;
2020-03-31 15:19:53 +02:00
return ` ${ ( sf || { fileName : "unknown" } ).fileName} ( ${ line + 1 } , ${ character + 1 } ) ` ;
} ;
function report ( node : ts.Node , message : string ) {
console . log ( ` ${ source_location ( node ) } : ${ message } ` ) ;
2018-12-15 14:04:29 +01:00
}
function _generate ( config : Configuration , node : ts.Node , result : TranslationEntry [ ] ) {
//console.log("Node: %s", SyntaxKind[node.kind]);
call_analize :
if ( ts . isCallExpression ( node ) ) {
2020-08-01 14:54:44 +02:00
const call = node as ts . CallExpression ;
2018-12-15 14:04:29 +01:00
const call_name = call . expression [ "escapedText" ] as string ;
2020-08-01 14:54:44 +02:00
if ( call_name != "tr" ) {
break call_analize ;
}
2018-12-15 14:04:29 +01:00
console . dir ( call_name ) ;
console . log ( "Parameters: %o" , call . arguments . length ) ;
if ( call . arguments . length > 1 ) {
report ( call , "Invalid argument count" ) ;
2020-08-01 14:54:44 +02:00
break call_analize ;
2018-12-15 14:04:29 +01:00
}
const object = < ts.StringLiteral > call . arguments [ 0 ] ;
if ( object . kind != SyntaxKind . StringLiteral ) {
report ( call , "Invalid argument: " + SyntaxKind [ object . kind ] ) ;
2020-08-01 14:54:44 +02:00
break call_analize ;
2018-12-15 14:04:29 +01:00
}
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 ,
2020-08-01 14:54:44 +02:00
message : object.text ,
type : "call"
} ) ;
} else if ( node . kind === SyntaxKind . JsxElement ) {
const element = node as ts . JsxElement ;
const tag = element . openingElement . tagName as ts . Identifier ;
if ( tag . kind !== SyntaxKind . Identifier )
break call_analize ;
2020-08-01 15:04:33 +02:00
if ( tag . escapedText === "Translatable" ) {
if ( element . children . length !== 1 ) {
report ( element , "Invalid child count: " + element . children . length ) ;
break call_analize ;
}
2020-08-01 14:54:44 +02:00
2020-08-01 15:04:33 +02:00
const text = element . children [ 0 ] as ts . JsxText ;
if ( text . kind != SyntaxKind . JsxText ) {
report ( element , "Invalid child type " + SyntaxKind [ text . kind ] ) ;
break call_analize ;
}
2020-08-01 14:54:44 +02:00
2020-08-01 15:04:33 +02:00
const { line , character } = node . getSourceFile ( ) . getLineAndCharacterOfPosition ( node . getStart ( ) ) ;
result . push ( {
filename : node.getSourceFile ( ) . fileName ,
line : line ,
character : character ,
message : text.text ,
type : "jsx-translatable"
} ) ;
} else if ( tag . escapedText === "VariadicTranslatable" ) {
2020-08-01 14:54:44 +02:00
2020-08-01 15:04:33 +02:00
}
2018-12-15 14:04:29 +01:00
}
node . forEachChild ( n = > _generate ( config , n , result ) ) ;
}
2020-08-01 14:54:44 +02:00
function generateUniqueCheck ( config : Configuration , source_file : ts.SourceFile , variable : ts.Expression , variables : { name : string , node : ts.Node } [ ] ) : ts . Node [ ] {
2018-12-15 14:04:29 +01:00
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 */
{
2020-04-01 21:47:33 +02:00
const declarations = ts . createElementAccess ( variable , ts . createLiteral ( config . variables . declarations ) ) ;
2018-12-15 14:04:29 +01:00
nodes . push ( ts . createAssignment ( declarations , ts . createBinary ( declarations , SyntaxKind . BarBarToken , ts . createAssignment ( declarations , ts . createObjectLiteral ( ) ) ) ) ) ;
2020-04-01 21:47:33 +02:00
declarations_file = ts . createElementAccess ( variable , ts . createLiteral ( config . variables . declare_files ) ) ;
2018-12-15 14:04:29 +01:00
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 ( [
2020-04-01 21:47:33 +02:00
ts . createBindingElement ( undefined , config . optimized ? "n" : "name" , for_variable_name , undefined ) ,
ts . createBindingElement ( undefined , config . optimized ? "p" : "path" , for_variable_path , undefined ) ] )
2018-12-15 14:04:29 +01:00
, 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 ( [
2020-04-01 21:47:33 +02:00
ts . createPropertyAssignment ( config . optimized ? "n" : "name" , ts . createLiteral ( e . name ) ) ,
ts . createPropertyAssignment ( config . optimized ? "p" : "path" , ts . createLiteral ( node_path ( e . node ) ) )
2018-12-15 14:04:29 +01:00
] ) )
] )
, 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 ) ) ] ;
}
2020-08-01 14:54:44 +02:00
let globalIdIndex = 0 , globalIdTimestamp = Date . now ( ) ;
2020-03-31 15:19:53 +02:00
export function transform ( config : Configuration , context : ts.TransformationContext , source_file : ts.SourceFile ) : TransformResult {
2018-12-15 14:04:29 +01:00
const cache : VolatileTransformConfig = { } as any ;
cache . translations = [ ] ;
2020-04-01 21:47:33 +02:00
config . variables = ( config . variables || { } ) as any ;
config . variables . base = config . variables . base || ( config . optimized ? "__tr" : "_translations" ) ;
config . variables . declare_files = config . variables . declare_files || ( config . optimized ? "f" : "declare_files" ) ;
config . variables . declarations = config . variables . declarations || ( config . optimized ? "d" : "definitions" ) ;
2018-12-15 14:04:29 +01:00
//Initialize nodes
const extra_nodes : ts.Node [ ] = [ ] ;
{
cache . nodes = { } as any ;
if ( config . use_window ) {
const window = ts . createIdentifier ( "window" ) ;
2020-04-01 21:47:33 +02:00
let translation_map = ts . createPropertyAccess ( window , ts . createIdentifier ( config . variables . base ) ) ;
2018-12-15 14:04:29 +01:00
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 ) ;
2020-04-01 21:47:33 +02:00
extra_nodes . push ( translation_map_init ) ;
2018-12-15 14:04:29 +01:00
cache . nodes = {
2020-04-01 21:47:33 +02:00
translation_map : translation_map
} ;
} else if ( config . module ) {
cache . nodes = {
translation_map : ts.createIdentifier ( config . variables . base )
2018-12-15 14:04:29 +01:00
} ;
2020-04-01 21:47:33 +02:00
extra_nodes . push ( ts . createVariableDeclarationList ( [
ts . createVariableDeclaration ( config . variables . base , undefined , ts . createObjectLiteral ( ) )
] , ts . NodeFlags . Const ) , ts . createToken ( SyntaxKind . SemicolonToken ) ) ;
} else {
const variable_map = ts . createIdentifier ( config . variables . base ) ;
2018-12-15 14:04:29 +01:00
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 ,
} ;
extra_nodes . push ( inline_if ) ;
}
}
const generated_names : { name : string , node : ts.Node } [ ] = [ ] ;
2020-04-01 21:47:33 +02:00
let generator_base = 0 ;
2018-12-15 14:04:29 +01:00
2020-08-01 14:54:44 +02:00
const generate_unique_name = config = > {
if ( config . module ) {
return "_" + generator_base ++ ;
} else {
return "_" + globalIdTimestamp + "-" + ++ globalIdIndex ;
}
} ;
cache . name_generator = ( config , node , message ) = > {
const name = generate_unique_name ( config ) ;
2018-12-15 14:04:29 +01:00
generated_names . push ( { name : name , node : node } ) ;
return name ;
} ;
2020-08-01 14:54:44 +02:00
cache . tsx_name_generator = generate_unique_name ;
2018-12-15 14:04:29 +01:00
function visit ( node : ts.Node ) : ts . Node {
node = ts . visitEachChild ( node , visit , context ) ;
2020-03-31 15:19:53 +02:00
return replace_processor ( config , cache , node , source_file ) ;
2018-12-15 14:04:29 +01:00
}
2020-03-31 15:19:53 +02:00
source_file = ts . visitNode ( source_file , visit ) ;
2020-04-01 21:47:33 +02:00
if ( ! config . module ) {
/* we don't need a unique check because we're just in our scope */
2020-08-01 14:54:44 +02:00
extra_nodes . push ( . . . generateUniqueCheck ( config , source_file , cache . nodes . translation_map , generated_names ) ) ;
2020-04-01 21:47:33 +02:00
}
2018-12-15 14:04:29 +01:00
2020-03-31 15:19:53 +02:00
source_file = ts . updateSourceFileNode ( source_file , [ . . . ( extra_nodes as any [ ] ) , . . . source_file . statements ] , source_file . isDeclarationFile , source_file . referencedFiles , source_file . typeReferenceDirectives , source_file . hasNoDefaultLib , source_file . referencedFiles ) ;
2018-12-15 14:04:29 +01:00
const result : TransformResult = { } as any ;
2020-03-31 15:19:53 +02:00
result . node = source_file ;
2018-12-15 14:04:29 +01:00
result . translations = cache . translations ;
return result ;
}
2020-08-01 15:04:33 +02:00
const generate_jsx_cache_key = ( cache : VolatileTransformConfig , config : Configuration , element : ts.JsxElement ) = > ts . updateJsxElement (
element ,
ts . updateJsxOpeningElement (
element . openingElement ,
element . openingElement . tagName ,
element . openingElement . typeArguments ,
ts . updateJsxAttributes ( element . openingElement . attributes , [
. . . element . openingElement . attributes . properties ,
ts . createJsxAttribute ( ts . createIdentifier ( "__cacheKey" ) , ts . createStringLiteral ( cache . tsx_name_generator ( config ) ) )
] )
) ,
element . children ,
element . closingElement
) ;
2020-03-31 15:19:53 +02:00
export function replace_processor ( config : Configuration , cache : VolatileTransformConfig , node : ts.Node , source_file : ts.SourceFile ) : ts . Node {
2018-12-15 14:04:29 +01:00
if ( config . verbose )
console . log ( "Process %s" , SyntaxKind [ node . kind ] ) ;
2020-08-01 14:54:44 +02:00
2020-08-01 15:27:45 +02:00
if ( ! node . getSourceFile ( ) )
return node ;
2018-12-15 14:04:29 +01:00
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 ) ;
}
2020-03-31 15:19:53 +02:00
if ( call . arguments . length > 1 )
throw new Error ( source_location ( call ) + ": tr(...) has been called with an invalid arguments (" + ( call . arguments . length === 0 ? "too few" : "too many" ) + ")" ) ;
const fullText = call . getFullText ( source_file ) ;
if ( fullText && fullText . indexOf ( "@tr-ignore" ) !== - 1 )
2018-12-15 14:04:29 +01:00
return node ;
const object = < ts.StringLiteral > call . arguments [ 0 ] ;
if ( object . kind != SyntaxKind . StringLiteral ) {
2020-03-31 15:19:53 +02:00
if ( call . getSourceFile ( ) )
throw new Error ( source_location ( call ) + ": Ignoring tr call because given argument isn't of type string literal. (" + SyntaxKind [ object . kind ] + ")" ) ;
report ( call , "Ignoring tr call because given argument isn't of type string literal. (" + SyntaxKind [ object . kind ] + ")" ) ;
2018-12-15 14:04:29 +01:00
}
if ( config . verbose )
2020-03-31 15:19:53 +02:00
console . log ( "Message: %o" , object . text || object . getText ( source_file ) ) ;
2018-12-15 14:04:29 +01:00
2020-03-31 15:19:53 +02:00
const variable_name = ts . createIdentifier ( cache . name_generator ( config , node , object . text || object . getText ( source_file ) ) ) ;
2020-04-01 21:47:33 +02:00
const variable_init = ts . createPropertyAccess ( cache . nodes . translation_map , variable_name ) ;
2018-12-15 14:04:29 +01:00
const variable = ts . createPropertyAccess ( cache . nodes . translation_map , variable_name ) ;
const new_variable = ts . createAssignment ( variable , call ) ;
2020-03-31 15:19:53 +02:00
let { line , character } = source_file . getLineAndCharacterOfPosition ( node . getStart ( ) ) ;
2018-12-15 14:04:29 +01:00
cache . translations . push ( {
2020-03-31 15:19:53 +02:00
message : object.text || object . getText ( source_file ) ,
2018-12-15 14:04:29 +01:00
line : line ,
character : character ,
2020-08-01 14:54:44 +02:00
filename : ( source_file || { fileName : "unknown" } ) . fileName ,
type : "call"
2018-12-15 14:04:29 +01:00
} ) ;
return ts . createBinary ( variable_init , ts . SyntaxKind . BarBarToken , new_variable ) ;
2020-08-01 14:54:44 +02:00
} else if ( node . kind === SyntaxKind . JsxElement ) {
const element = node as ts . JsxElement ;
const tag = element . openingElement . tagName as ts . Identifier ;
if ( tag . kind !== SyntaxKind . Identifier )
return node ;
2020-08-01 15:04:33 +02:00
const properties = { } as any ;
2020-08-01 14:54:44 +02:00
2020-08-01 15:04:33 +02:00
element . openingElement . attributes . properties . forEach ( ( e : ts.JsxAttribute ) = > {
if ( e . kind !== SyntaxKind . JsxAttribute )
throw new Error ( source_location ( e ) + ": Invalid jsx attribute kind " + SyntaxKind [ e . kind ] ) ;
2020-08-01 14:54:44 +02:00
2020-08-01 15:04:33 +02:00
if ( e . name . kind !== SyntaxKind . Identifier )
throw new Error ( source_location ( e ) + ": Key isn't an identifier" ) ;
2020-08-01 14:54:44 +02:00
2020-08-01 15:04:33 +02:00
properties [ e . name . escapedText as string ] = e . initializer ;
} ) ;
2020-08-01 14:54:44 +02:00
2020-08-01 15:04:33 +02:00
if ( tag . escapedText === "Translatable" ) {
2020-08-01 14:54:44 +02:00
if ( 'trIgnore' in properties && properties . trIgnore . kind === SyntaxKind . JsxExpression ) {
const ignoreAttribute = properties . trIgnore as ts . JsxExpression ;
if ( ignoreAttribute . expression . kind === SyntaxKind . TrueKeyword )
return node ;
else if ( ignoreAttribute . expression . kind !== SyntaxKind . FalseKeyword )
throw new Error ( source_location ( ignoreAttribute ) + ": Invalid attribute value of type " + SyntaxKind [ ignoreAttribute . expression . kind ] ) ;
}
if ( element . children . length !== 1 )
throw new Error ( source_location ( element ) + ": Element has been called with an invalid arguments (" + ( element . children . length === 0 ? "too few" : "too many" ) + ")" ) ;
const text = element . children [ 0 ] as ts . JsxText ;
if ( text . kind != SyntaxKind . JsxText )
throw new Error ( source_location ( element ) + ": Element has invalid children. Expected JsxText but got " + SyntaxKind [ text . kind ] ) ;
let { line , character } = source_file . getLineAndCharacterOfPosition ( node . getStart ( ) ) ;
cache . translations . push ( {
message : text.text ,
line : line ,
character : character ,
filename : ( source_file || { fileName : "unknown" } ) . fileName ,
type : "jsx-translatable"
} ) ;
2020-08-01 15:04:33 +02:00
return generate_jsx_cache_key ( cache , config , element ) ;
} else if ( tag . escapedText === "VariadicTranslatable" ) {
if ( ! ( 'text' in properties ) )
throw new Error ( source_location ( element ) + ": Missing text to translate" ) ;
const textAttribute = properties [ "text" ] as ts . JsxExpression ;
if ( textAttribute . kind !== SyntaxKind . JsxExpression )
throw new Error ( source_location ( element ) + ": Text attribute has an invalid type. Expected JsxExpression but received " + SyntaxKind [ textAttribute . kind ] ) ;
if ( textAttribute . expression . kind !== SyntaxKind . StringLiteral )
throw new Error ( source_location ( element ) + ": Text attribute value isn't a string literal. Expected StringLiteral but received " + SyntaxKind [ textAttribute . expression . kind ] ) ;
const literal = textAttribute . expression as ts . StringLiteral ;
let { line , character } = source_file . getLineAndCharacterOfPosition ( node . getStart ( ) ) ;
cache . translations . push ( {
message : literal.text ,
line : line ,
character : character ,
filename : ( source_file || { fileName : "unknown" } ) . fileName ,
type : "jsx-variadic-translatable"
} ) ;
return generate_jsx_cache_key ( cache , config , element ) ;
2020-08-01 14:54:44 +02:00
}
2018-12-15 14:04:29 +01:00
}
2020-08-01 14:54:44 +02:00
2018-12-15 14:04:29 +01:00
return node ;
}
export interface Configuration {
use_window? : boolean ;
replace_cache? : boolean ;
verbose? : boolean ;
2020-04-01 21:47:33 +02:00
optimized? : boolean ;
module? : boolean ;
variables ? : {
base : string ,
declarations : string ,
declare_files : string
}
2018-12-15 14:04:29 +01:00
}
export interface TransformResult {
node : ts.SourceFile ;
translations : TranslationEntry [ ] ;
}
interface VolatileTransformConfig {
nodes : {
translation_map : ts.Expression ;
} ;
name_generator : ( config : Configuration , node : ts.Node , message : string ) = > string ;
2020-08-01 14:54:44 +02:00
tsx_name_generator : ( config : Configuration ) = > string ;
2018-12-15 14:04:29 +01:00
translations : TranslationEntry [ ] ;
}