2019-03-27 08:49:29 +00:00
'use strict' ;
// Processes statements like these:
// tUI(/*prefix:account*/'account.passwordChangeRequest', language)
// /*prefix:helpers*/<Trans i18nKey="userMessagesUnread" count={count}>Hello <strong title={t('nameTitle')}>{{name}}</strong>, you have {{count}} unread message. <Link to="/msgs">Go to messages</Link>.</Trans>
const fs = require ( 'fs' ) ;
const path = require ( 'path' ) ;
const klawSync = require ( 'klaw-sync' ) ;
const acorn = require ( "acorn" ) ;
const acornJsx = require ( "acorn-jsx" ) ;
const ellipsize = require ( 'ellipsize' ) ;
const camelCase = require ( 'camelcase' ) ;
const slugify = require ( 'slugify' ) ;
const readline = require ( 'readline' ) ;
2019-05-12 17:52:39 +00:00
const deepKeys = require ( 'deep-keys' ) ;
2019-03-27 08:49:29 +00:00
2019-05-12 17:52:39 +00:00
const localeMain = 'en-US/common.json' ;
2019-05-12 22:45:36 +00:00
const localeMainPrevious = 'en-US-last-run/common.json' ;
2019-11-09 16:47:58 +00:00
const localeTranslations = [ 'es-ES/common.json' , 'pt-BR/common.json' , 'de-DE/common.json' ] ;
2019-03-27 08:49:29 +00:00
const searchDirs = [
'../client/src' ,
'../server' ,
'../shared'
] ;
2019-05-12 17:52:39 +00:00
const todoMarker = " - TODO: update line above and then delete this line to mark that the translation has been fixed" ;
const renamedKeys = new Map ( ) ;
const keysWithChangedValue = new Set ( ) ;
const resDict = { } ;
let anyUpdatesToResDict = false ;
const origResDict = JSON . parse ( fs . readFileSync ( localeMain ) ) ;
const prevResDict = JSON . parse ( fs . readFileSync ( localeMainPrevious ) ) ;
2019-03-27 08:49:29 +00:00
function findAllVariantsByPrefixInDict ( dict , keyPrefix ) {
const keyElems = keyPrefix . split ( '.' ) ;
for ( const keyElem of keyElems . slice ( 0 , - 1 ) ) {
if ( dict [ keyElem ] ) {
if ( typeof dict [ keyElem ] === 'string' ) {
return [ ] ;
} else {
dict = dict [ keyElem ] ;
}
} else {
return [ ] ;
}
}
const prefix = keyElems [ keyElems . length - 1 ] ;
const res = [ ] ;
for ( const key in dict ) {
if ( key . startsWith ( prefix ) ) {
res . push ( key . substring ( prefix . length ) ) ;
}
}
return res ;
}
function findInDict ( dict , key ) {
const keyElems = key . split ( '.' ) ;
for ( const keyElem of keyElems . slice ( 0 , - 1 ) ) {
if ( dict [ keyElem ] ) {
if ( typeof dict [ keyElem ] === 'string' ) {
return undefined ;
} else {
dict = dict [ keyElem ] ;
}
} else {
return undefined ;
}
}
return dict [ keyElems [ keyElems . length - 1 ] ] ;
}
function setInDict ( dict , key , value ) {
const keyElems = key . split ( '.' ) ;
for ( const keyElem of keyElems . slice ( 0 , - 1 ) ) {
if ( dict [ keyElem ] ) {
if ( typeof dict [ keyElem ] === 'string' ) {
throw new Error ( ` Overlapping key ${ key } ` ) ;
}
} else {
dict [ keyElem ] = { }
}
dict = dict [ keyElem ] ;
}
dict [ keyElems [ keyElems . length - 1 ] ] = value ;
}
const assignedKeys = new Map ( ) ;
function getKeyFromValue ( spec , value ) {
let key = value . replace ( /<\/?[0-9]+>/g , '' ) ; // Remove Trans markup
key = slugify ( key , { replacement : ' ' , remove : /[\\()"':.,;\/\[\]\{\}*+-]/g , lower : false } ) ;
key = camelCase ( key ) ;
key = ellipsize ( key , 40 , {
chars : [ ... Array ( 26 ) ] . map ( ( _ , i ) => String . fromCharCode ( 'A' . charCodeAt ( 0 ) + i ) ) /* This is an array of characters A-Z */ ,
ellipse : ''
} ) ;
if ( spec . prefix ) {
key = spec . prefix + '.' + key ;
}
let idx = 0 ;
while ( true ) {
const keyExt = key + ( idx ? '-' + idx : '' )
if ( assignedKeys . has ( keyExt ) ) {
if ( assignedKeys . get ( keyExt ) === value ) {
return keyExt ;
}
} else {
assignedKeys . set ( keyExt , value ) ;
return keyExt ;
}
idx ++ ;
}
}
function allowedDirOrFile ( item ) {
const pp = path . parse ( item . path )
return (
( item . stats . isDirectory ( ) &&
pp . base !== 'node_modules'
) ||
( item . stats . isFile ( ) &&
( pp . ext === '.js' || pp . ext === '.jsx' || pp . ext === '.hbs' )
)
) ;
}
function parseSpec ( specStr ) {
const spec = { } ;
if ( specStr ) {
const entryMatcher = /([a-zA-Z]*)\s*:\s*(.*)/
const entries = specStr . split ( /\s*,\s*/ ) ;
for ( const entry of entries ) {
const elems = entry . match ( entryMatcher ) ;
if ( elems ) {
spec [ elems [ 1 ] ] = elems [ 2 ] ;
} else {
spec [ entry ] = true ;
}
}
}
return spec ;
}
// see http://blog.stevenlevithan.com/archives/match-quoted-string
const tMatcher = /(^|[ {+(=.\[])((?:tUI|tLog|t|tMark)\s*\(\s*(?:\/\*(.*?)\*\/)?\s*)(["'])((?:(?!\4)[^\\]|\\.)*)(\4)/ ;
const jsxTransMatcher = /(\/\*(.*?)\*\/\s*)?(\<Trans[ >][\s\S]*?\<\/Trans\>)/ ;
const hbsTranslateMatcher = /(\{\{!--(.*?)--\}\}\s*)?(\{\{#translate\}\})([\s\S]*?)(\{\{\/translate\}\})/ ;
const jsxParser = acorn . Parser . extend ( acornJsx ( ) ) ;
function parseJsxTrans ( fragment ) {
const match = fragment . match ( jsxTransMatcher ) ;
const spec = parseSpec ( match [ 2 ] ) ;
const jsxStr = match [ 3 ] ;
const jsxStrSmpl = jsxStr . replace ( '{::' , '{ ' ) ; // Acorn does not handle bind (::) operator. So we just leave it out because we are not interested in the code anyway.
const ast = jsxParser . parse ( jsxStrSmpl ) ;
function convertChildren ( children ) {
const entries = [ ] ;
let childNo = 0 ;
for ( const child of children ) {
const type = child . type ;
if ( type === 'JSXText' ) {
entries . push ( child . value ) ;
childNo ++ ;
} else if ( type === 'JSXElement' ) {
const inner = convertChildren ( child . children ) ;
entries . push ( ` < ${ childNo } > ${ convertChildren ( child . children ) } </ ${ childNo } > ` ) ;
childNo ++ ;
} else if ( type === 'JSXExpressionContainer' ) {
entries . push ( jsxStr . substring ( child . start , child . end ) ) ;
childNo ++ ;
} else {
throw new Error ( 'Unknown JSX node: ' + child ) ;
}
}
return entries . join ( '' ) ;
}
const expr = ast . body [ 0 ] . expression ;
let originalKey ;
for ( const attr of expr . openingElement . attributes ) {
const name = attr . name . name ;
if ( name === 'i18nKey' ) {
originalKey = attr . value . value ;
}
}
const convValue = convertChildren ( expr . children ) ;
if ( originalKey === undefined ) {
originalKey = convValue ;
}
let value ;
2019-05-12 17:52:39 +00:00
const originalValue = findInDict ( origResDict , originalKey ) ;
2019-03-27 08:49:29 +00:00
if ( originalValue === undefined ) {
value = convValue ;
2019-05-12 22:35:19 +00:00
originalKey = undefined ;
2019-03-27 08:49:29 +00:00
} else {
value = originalValue ;
}
const key = getKeyFromValue ( spec , value ) ;
const replacement = ` ${ match [ 1 ] || '' } <Trans i18nKey=" ${ key } "> ${ jsxStr . substring ( expr . openingElement . end , expr . closingElement . start ) } </Trans> ` ;
2019-05-12 22:35:19 +00:00
return { key , originalKey , value , replacement } ;
2019-03-27 08:49:29 +00:00
}
function parseHbsTranslate ( fragment ) {
const match = fragment . match ( hbsTranslateMatcher ) ;
const spec = parseSpec ( match [ 2 ] ) ;
2019-05-12 22:35:19 +00:00
let originalKey = match [ 4 ] ;
2019-03-27 08:49:29 +00:00
let value ;
2019-05-12 17:52:39 +00:00
const originalValue = findInDict ( origResDict , originalKey ) ;
2019-03-27 08:49:29 +00:00
if ( originalValue === undefined ) {
value = originalKey ;
2019-05-12 22:35:19 +00:00
originalKey = undefined ;
2019-03-27 08:49:29 +00:00
} else {
value = originalValue ;
}
const key = getKeyFromValue ( spec , value ) ;
const replacement = ` ${ match [ 1 ] || '' } ${ match [ 3 ] } ${ key } ${ match [ 5 ] } ` ;
2019-05-12 17:52:39 +00:00
return { key , originalKey , value , replacement , originalValue } ;
2019-03-27 08:49:29 +00:00
}
function parseT ( fragment ) {
const match = fragment . match ( tMatcher ) ;
2019-05-12 22:35:19 +00:00
let originalKey = match [ 5 ] ;
2019-03-27 08:49:29 +00:00
const spec = parseSpec ( match [ 3 ] ) ;
if ( spec . ignore ) {
return null ;
}
let value ;
2019-05-12 17:52:39 +00:00
const originalValue = findInDict ( origResDict , originalKey ) ;
2019-03-27 08:49:29 +00:00
if ( originalValue === undefined ) {
value = originalKey ;
2019-05-12 22:35:19 +00:00
originalKey = undefined ;
2019-03-27 08:49:29 +00:00
} else {
value = originalValue ;
}
const key = getKeyFromValue ( spec , value ) ;
const replacement = ` ${ match [ 1 ] } ${ match [ 2 ] } ${ match [ 4 ] } ${ key } ${ match [ 6 ] } ` ;
2019-05-12 17:52:39 +00:00
return { key , originalKey , value , replacement , originalValue } ;
2019-03-27 08:49:29 +00:00
}
function processFile ( file ) {
let source = fs . readFileSync ( file , 'utf8' ) ;
let anyUpdates = false ;
function update ( fragments , parseFun ) {
if ( fragments ) {
for ( const fragment of fragments ) {
const parseStruct = parseFun ( fragment ) ;
if ( parseStruct ) {
2019-05-12 22:35:19 +00:00
const { key , originalKey , value , replacement } = parseStruct ;
2019-03-27 08:49:29 +00:00
source = source . split ( fragment ) . join ( replacement ) ;
setInDict ( resDict , key , value ) ;
2019-05-12 23:07:01 +00:00
if (
// If a key is formed from a value (e.g. t('Refresh') ) and and the same time already present in common.json, originalKey is undefined. Therefore the additional test here
( originalKey === undefined && findInDict ( origResDict , key ) !== value ) ||
( originalKey !== undefined && findInDict ( prevResDict , originalKey ) !== value )
) {
2019-05-12 22:35:19 +00:00
keysWithChangedValue . add ( key ) ;
anyUpdates = true ;
}
2019-05-12 17:52:39 +00:00
2019-05-12 22:35:19 +00:00
const variants = originalKey !== undefined ? findAllVariantsByPrefixInDict ( origResDict , originalKey + '_' ) : [ ] ;
2019-03-27 08:49:29 +00:00
for ( const variant of variants ) {
2019-05-12 17:52:39 +00:00
const variantKey = originalKey + '_' + variant ;
const variantValue = findInDict ( origResDict , variantKey ) ;
const prevVariantValue = findInDict ( prevResDict , variantKey ) ;
setInDict ( resDict , key + '_' + variant , variantValue ) ;
if ( prevVariantValue !== variantValue ) {
keysWithChangedValue . add ( variantKey ) ;
anyUpdates = true ;
}
2019-03-27 08:49:29 +00:00
}
2019-05-12 22:35:19 +00:00
if ( originalKey !== undefined && originalKey !== key ) {
2019-05-12 17:52:39 +00:00
renamedKeys . set ( key , originalKey ) ;
2019-03-27 08:49:29 +00:00
for ( const variant of variants ) {
2019-05-12 17:52:39 +00:00
renamedKeys . set ( key + '_' + variant , originalKey + '_' + variant ) ;
2019-03-27 08:49:29 +00:00
}
}
2019-05-12 17:52:39 +00:00
if ( originalKey !== key ) {
2019-03-27 08:49:29 +00:00
anyUpdates = true ;
}
}
}
}
}
const lines = source . split ( /\r?\n/g ) ;
for ( const line of lines ) {
const fragments = line . match ( new RegExp ( tMatcher , 'g' ) ) ;
update ( fragments , parseT ) ;
}
const hbsFragments = source . match ( new RegExp ( hbsTranslateMatcher , 'g' ) ) ;
update ( hbsFragments , parseHbsTranslate ) ;
const jsxFragments = source . match ( new RegExp ( jsxTransMatcher , 'g' ) ) ;
update ( jsxFragments , parseJsxTrans ) ;
if ( anyUpdates ) {
console . log ( ` Updating ${ file } ` ) ;
fs . writeFileSync ( file , source ) ;
anyUpdatesToResDict = true ;
}
}
function run ( ) {
for ( const dir of searchDirs ) {
2019-05-12 17:52:39 +00:00
const files = klawSync ( dir , { nodir : true , filter : allowedDirOrFile } ) ;
2019-03-27 08:49:29 +00:00
for ( const file of files ) {
processFile ( file . path ) ;
}
}
if ( anyUpdatesToResDict ) {
2019-05-12 17:52:39 +00:00
console . log ( ` Updating ${ localeMain } ` ) ;
fs . writeFileSync ( localeMain , JSON . stringify ( resDict , null , 2 ) ) ;
2019-05-12 22:45:36 +00:00
console . log ( ` Updating ${ localeMainPrevious } ` ) ;
fs . writeFileSync ( localeMainPrevious , JSON . stringify ( resDict , null , 2 ) ) ;
2019-05-12 17:52:39 +00:00
}
const mainKeys = deepKeys ( resDict ) ;
for ( const localeTranslation of localeTranslations ) {
const origTransResDict = JSON . parse ( fs . readFileSync ( localeTranslation ) ) ;
const origTransKeys = deepKeys ( origTransResDict ) ;
let isEq = origTransKeys . length === mainKeys . size ;
if ( isEq ) {
for ( const origKey in origTransKeys ) {
if ( ! mainKeys . has ( origKey ) ) {
isEq = false ;
break ;
}
}
}
if ( ! isEq || anyUpdatesToResDict ) {
console . log ( ` Updating ${ localeTranslation } ` ) ;
const transResDict = { } ;
for ( const key of mainKeys ) {
let origKey = key ;
if ( renamedKeys . has ( key ) ) {
origKey = renamedKeys . get ( key ) ;
}
const origValue = findInDict ( origTransResDict , origKey ) ;
const origTodoValue = findInDict ( origTransResDict , origKey + todoMarker ) ;
const prevMainValue = findInDict ( prevResDict , origKey ) ;
const mainValue = findInDict ( resDict , key ) ;
const isChanged = keysWithChangedValue . has ( key ) ;
if ( origValue === undefined || ( isChanged && prevMainValue === origValue ) ) {
setInDict ( transResDict , key , mainValue ) ;
} else {
setInDict ( transResDict , key , origValue ) ;
}
if ( isChanged || origValue === undefined || origTodoValue !== undefined ) {
setInDict ( transResDict , key + todoMarker , mainValue ) ;
}
}
fs . writeFileSync ( localeTranslation , JSON . stringify ( transResDict , null , 2 ) ) ;
}
2019-03-27 08:49:29 +00:00
}
}
const rl = readline . createInterface ( {
input : process . stdin ,
output : process . stdout
} ) ;
console . log ( 'This script does modifications in the source tree. You should first commit all your files in git before proceeding.' ) ;
rl . question ( 'To proceed type YES: ' , ( answer ) => {
if ( answer === 'YES' ) {
run ( ) ;
}
rl . close ( ) ;
} ) ;