import * as ts from "typescript" ; import sha256 from "sha256" ; import { SyntaxKind } from "typescript" ; import { TranslationEntry } from "../Definitions" ; const getSourceLocation = ( 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 })` ; } ; function report ( node , message ) { console . log ( `${ getSourceLocation ( node ) }: ${ message }` ) ; } function generateUniqueCheck ( config , source_file , variable , variables ) { const nodes = [ ] , blockedNodes = [ ] ; const nodePath = ( 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 createError = ( variable_name , variable_path , other_path ) => { 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 declarationsFile ; const uniqueCheckLabelName = "unique_translation_check" ; { const declarations = ts . createElementAccess ( variable , ts . createLiteral ( config . variables . declarations ) ) ; nodes . push ( ts . createAssignment ( declarations , ts . createBinary ( declarations , SyntaxKind . BarBarToken , ts . createAssignment ( declarations , ts . createObjectLiteral ( ) ) ) ) ) ; declarationsFile = ts . createElementAccess ( variable , ts . createLiteral ( config . variables . declareFiles ) ) ; nodes . push ( ts . createAssignment ( declarationsFile , ts . createBinary ( declarationsFile , SyntaxKind . BarBarToken , ts . createAssignment ( declarationsFile , ts . createObjectLiteral ( ) ) ) ) ) ; variable = declarations ; } { const uniqueId = sha256 ( source_file . fileName + " | " + ( Date . now ( ) / 1000 ) ) ; const property = ts . createElementAccess ( declarationsFile , ts . createLiteral ( uniqueId ) ) ; const ifCondition = ts . createBinary ( property , SyntaxKind . ExclamationEqualsEqualsToken , ts . createIdentifier ( "undefined" ) ) ; let ifThen ; { const elements = [ ] ; 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 ( uniqueCheckLabelName ) ) ; ifThen = ts . createBlock ( elements ) ; } const ifElse = ts . createAssignment ( property , ts . createLiteral ( uniqueId ) ) ; blockedNodes . push ( ts . createIf ( ifCondition , ifThen , ifElse as any ) ) ; } { const forVariableName = ts . createLoopVariable ( ) ; const forVariablePath = ts . createLoopVariable ( ) ; const forDeclaration = ts . createVariableDeclarationList ( [ ts . createVariableDeclaration ( ts . createObjectBindingPattern ( [ ts . createBindingElement ( undefined , config . optimized ? "n" : "name" , forVariableName , undefined ) , ts . createBindingElement ( undefined , config . optimized ? "p" : "path" , forVariablePath , undefined ) ] ) , undefined , undefined ) ] ) ; let forBlock ; { const elements = [ ] ; const property = ts . createElementAccess ( variable , forVariableName ) ; const ifCondition = ts . createBinary ( property , SyntaxKind . ExclamationEqualsEqualsToken , ts . createIdentifier ( "undefined" ) ) ; const ifThen = ts . createThrow ( createError ( forVariableName , forVariablePath , property ) ) ; const ifElse = ts . createAssignment ( property , forVariablePath ) ; const ifValid = ts . createIf ( ifCondition , ifThen , ifElse as any ) ; elements . push ( ifValid ) ; forBlock = ts . createBlock ( elements ) ; } let block = ts . createForOf ( undefined , forDeclaration , ts . createArrayLiteral ( [ ... variables . map ( e => ts . createObjectLiteral ( [ ts . createPropertyAssignment ( config . optimized ? "n" : "name" , ts . createLiteral ( e . name ) ) , ts . createPropertyAssignment ( config . optimized ? "p" : "path" , ts . createLiteral ( nodePath ( e . node ) ) ) ] ) ) ] ) , forBlock ) ; block = ts . addSyntheticLeadingComment ( block , SyntaxKind . MultiLineCommentTrivia , "Auto generated helper for testing if the translation keys are unique" , true ) ; blockedNodes . push ( block ) ; } return [ ... nodes , ts . createLabel ( uniqueCheckLabelName , ts . createBlock ( blockedNodes ) ) ] ; } let globalIdIndex = 0 , globalIdTimestamp = Date . now ( ) ; export function transform ( config , context , sourceFile ) { const cache = { } as any ; cache . translations = [ ] ; config . variables = ( config . variables || { } ) as any ; config . variables . base = config . variables . base || ( config . optimized ? "__tr" : "_translations" ) ; config . variables . declareFiles = config . variables . declareFiles || ( config . optimized ? "f" : "declare_files" ) ; config . variables . declarations = config . variables . declarations || ( config . optimized ? "d" : "definitions" ) ; const nodeFactory = ts . factory ; const extraNodes = [ ] ; if ( config . cacheTranslations ) { cache . nodes = { } as any ; if ( config . useWindow ) { const window = nodeFactory . createIdentifier ( "window" ) ; const translationMap = nodeFactory . createPropertyAccessExpression ( window , nodeFactory . createIdentifier ( config . variables . base ) ) ; const newTranslations = nodeFactory . createAssignment ( translationMap , nodeFactory . createObjectLiteralExpression ( ) ) ; extraNodes . push ( nodeFactory . createParenthesizedExpression ( nodeFactory . createBinaryExpression ( translationMap , ts . SyntaxKind . BarBarToken , newTranslations ) ) ) ; cache . nodes = { translationMap : translationMap } ; } else if ( config . module ) { cache . nodes = { translationMap : nodeFactory . createIdentifier ( config . variables . base ) } ; extraNodes . push ( ( nodeFactory . createVariableDeclarationList ( [ nodeFactory . createVariableDeclaration ( config . variables . base , undefined , undefined , nodeFactory . createObjectLiteralExpression ( ) ) ] , ts . NodeFlags . Const ) ) , nodeFactory . createToken ( SyntaxKind . SemicolonToken ) ) ; } else { const variableMap = nodeFactory . createIdentifier ( config . variables . base ) ; const inlineIf = nodeFactory . createBinaryExpression ( nodeFactory . createBinaryExpression ( nodeFactory . createTypeOfExpression ( variableMap ) , SyntaxKind . ExclamationEqualsEqualsToken , nodeFactory . createStringLiteral ( "undefined" ) ) , ts . SyntaxKind . BarBarToken , nodeFactory . createAssignment ( variableMap , nodeFactory . createObjectLiteralExpression ( ) ) ) ; cache . nodes = { translationMap : variableMap } ; extraNodes . push ( inlineIf ) ; } } const generatedNames = [ ] ; let generatorBase = 0 ; const generateUniqueName = config => { if ( config . module ) { return "_" + generatorBase ++ ; } else { return "_" + globalIdTimestamp + "-" + ++ globalIdIndex ; } } ; cache . nameGenerator = ( config , node ) => { const name = generateUniqueName ( config ) ; generatedNames . push ( { name : name , node : node } ) ; return name ; } ; cache . tsxNameGenerator = generateUniqueName ; function visit ( node ) { node = ts . visitEachChild ( node , visit , context ) ; return visitNode ( config , cache , node , sourceFile ) ; } sourceFile = ts . visitNode ( sourceFile , visit ) ; if ( ! config . module ) { extraNodes . push ( ... generateUniqueCheck ( config , sourceFile , cache . nodes . translationMap , generatedNames ) ) ; } if ( ! config . cacheTranslations ) { sourceFile = ts . updateSourceFileNode ( sourceFile , [ ... extraNodes , ... sourceFile . statements ] , sourceFile . isDeclarationFile , sourceFile . referencedFiles , sourceFile . typeReferenceDirectives , sourceFile . hasNoDefaultLib , sourceFile . referencedFiles ) ; } return { node : sourceFile , translations : cache . translations } ; } const generateJsxCacheKey = ( cache , config , element ) => 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 . tsxNameGenerator ( config ) ) ) ] ) ) , element . children , element . closingElement ) ; export function visitNode ( config , cache , node , sourceFile ) { if ( config . verbose ) { console . log ( "Process %s" , SyntaxKind [ node . kind ] ) ; } if ( ! node . getSourceFile ( ) ) { return node ; } if ( ts . isCallExpression ( node ) ) { const call = node as ts . CallExpression ; const callName = call . expression [ "escapedText" ] as string ; if ( callName !== "tr" && callName !== "useTr" ) { return node ; } if ( call . arguments . length > 1 ) { throw new Error ( getSourceLocation ( call ) + ": tr(...) has been called with an invalid arguments (" + ( call . arguments . length === 0 ? "too few" : "too many" ) + ")" ) ; } const fullText = call . getFullText ( sourceFile ) ; if ( fullText && fullText . indexOf ( "@tr-ignore" ) !== - 1 ) { return node ; } const object = < ts . StringLiteral > call . arguments [ 0 ] ; if ( object . kind != SyntaxKind . StringLiteral ) { if ( call . getSourceFile ( ) ) { throw new Error ( getSourceLocation ( 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 ] + ")" ) ; } if ( config . verbose ) { console . log ( "Message: %o" , object . text || object . getText ( sourceFile ) ) ; } let { line , character } = sourceFile . getLineAndCharacterOfPosition ( node . getStart ( ) ) ; cache . translations . push ( { message : object . text || object . getText ( sourceFile ) , line : line , character : character , filename : sourceFile . fileName , type : "call" } ) ; if ( ! config . cacheTranslations ) { return node ; } const variableName = ts . factory . createIdentifier ( cache . nameGenerator ( config , node , object . text || object . getText ( sourceFile ) ) ) ; const variableInit = ts . factory . createPropertyAccessExpression ( cache . nodes . translationMap , variableName ) ; const variable = ts . factory . createPropertyAccessExpression ( cache . nodes . translationMap , variableName ) ; const newVariable = ts . factory . createAssignment ( variable , call ) ; return ts . factory . createBinaryExpression ( variableInit , ts . SyntaxKind . BarBarToken , newVariable ) ; } 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 ; } const properties = { } ; element . openingElement . attributes . properties . forEach ( ( e ) => { if ( e . kind !== SyntaxKind . JsxAttribute ) { throw new Error ( getSourceLocation ( e ) + ": Invalid jsx attribute kind " + SyntaxKind [ e . kind ] ) ; } if ( e . name . kind !== SyntaxKind . Identifier ) { throw new Error ( getSourceLocation ( e ) + ": Key isn't an identifier" ) ; } properties [ e . name . escapedText as string ] = e . initializer ; } ) ; if ( tag . escapedText === "Translatable" ) { 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 ( getSourceLocation ( ignoreAttribute ) + ": Invalid attribute value of type " + SyntaxKind [ ignoreAttribute . expression . kind ] ) ; } } if ( element . children . length < 1 ) { throw new Error ( getSourceLocation ( element ) + ": Element has been called with an invalid arguments (too few)" ) ; } let text = element . children . map ( element => { if ( element . kind === SyntaxKind . JsxText ) { return element . text ; } else if ( element . kind === SyntaxKind . JsxSelfClosingElement ) { if ( element . tagName . kind !== SyntaxKind . Identifier ) { throw new Error ( getSourceLocation ( element . tagName ) + ": Expected a JsxSelfClosingElement, but received " + SyntaxKind [ element . tagName . kind ] ) ; } if ( element . tagName . escapedText !== "br" ) { throw new Error ( getSourceLocation ( element . tagName ) + ": Expected a br element, but received " + element . tagName . escapedText ) ; } return "\n" ; } } ) . join ( "" ) ; let { line , character } = sourceFile . getLineAndCharacterOfPosition ( node . getStart ( ) ) ; cache . translations . push ( { message : text , line : line , character : character , filename : sourceFile . fileName , type : "jsx-translatable" } ) ; if ( ! config . cacheTranslations ) { return node ; } return generateJsxCacheKey ( cache , config , element ) ; } else if ( tag . escapedText === "VariadicTranslatable" ) { if ( ! ( 'text' in properties ) ) { throw new Error ( getSourceLocation ( element ) + ": Missing text to translate" ) ; } const textAttribute = properties [ "text" ] as ts . JsxExpression ; if ( textAttribute . kind !== SyntaxKind . JsxExpression ) { throw new Error ( getSourceLocation ( element ) + ": Text attribute has an invalid type. Expected JsxExpression but received " + SyntaxKind [ textAttribute . kind ] ) ; } if ( textAttribute . expression . kind !== SyntaxKind . StringLiteral ) { throw new Error ( getSourceLocation ( 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 } = sourceFile . getLineAndCharacterOfPosition ( node . getStart ( ) ) ; cache . translations . push ( { message : literal . text , line : line , character : character , filename : sourceFile . fileName , type : "jsx-variadic-translatable" } ) ; if ( ! config . cacheTranslations ) { return node ; } return generateJsxCacheKey ( cache , config , element ) ; } } return node ; } export interface Configuration { useWindow ? : boolean ; cacheTranslations ? : boolean ; verbose ? : boolean ; optimized ? : boolean ; module ? : boolean ; variables ? : { base : string ; declarations : string ; declareFiles : string ; } ; } export interface TransformResult { node : ts . SourceFile ; translations : TranslationEntry [ ] ; } interface VolatileTransformConfig { nodes : { translationMap : ts . Expression ; } ; nameGenerator : ( config : Configuration , node : ts . Node , message : string ) => string ; tsxNameGenerator : ( config : Configuration ) => string ; translations : TranslationEntry [ ] ; }