import { Disposable , EventEmitter } from 'vscode' ; import { uniq , isObject } from 'lodash' ; import { LocaleTree , LocaleNode , LocaleRecord , FlattenLocaleTree } from '../Nodes' ; import { Coverage , FileInfo , PendingWrite , NodeOptions , RewriteKeySource , RewriteKeyContext , DataProcessContext } from '../types' ; import { Config , Global } from '..' ; import { resolveFlattenRootKeypath , resolveFlattenRoot , NodeHelper } from '~/utils' ; export abstract class Loader extends Disposable { protected _disposables : Disposable [ ] = [ ] ; protected _onDidChange = new EventEmitter < string > ( ) ; readonly onDidChange = this . _onDidChange . event ; protected _flattenLocaleTree : FlattenLocaleTree = { } ; protected _localeTree : LocaleTree = new LocaleTree ( { keypath : '' } ) ; constructor ( public readonly name ) { super ( ( ) => this . onDispose ( ) ) ; } abstract get locales ( ) : string [ ] ; get root ( ) { return this . _localeTree ; } get flattenLocaleTree ( ) { return this . _flattenLocaleTree ; } get files ( ) : FileInfo [ ] { return [ ] ; } get keys ( ) : string [ ] { return uniq ( Object . keys ( this . flattenLocaleTree ) ) ; } getCoverage ( locale , keys ? ) { const allKeys = keys || this . keys ; allKeys . sort ( ) ; const translatedKeys = allKeys . filter ( key => this . flattenLocaleTree [ key ] && this . flattenLocaleTree [ key ] . getValue ( locale ) ) ; const missingKeys = allKeys . filter ( key => ( ! this . flattenLocaleTree [ key ] ) || this . flattenLocaleTree [ key ] . getValue ( locale ) == null ) ; const emptyKeys = allKeys . filter ( key => ! translatedKeys . includes ( key ) && ! missingKeys . includes ( key ) ) ; const total = allKeys . length ; const translated = translatedKeys . length ; const missing = missingKeys . length ; return { locale , total , missing , translated , missingKeys , allKeys , translatedKeys , emptyKeys , } ; } protected updateTree ( tree , data , keypath , keyname , options , isCollection = false ) { tree = tree || new LocaleTree ( { keypath , keyname , isCollection , features : options . features , meta : options . meta , } ) ; tree . values [ options . locale ] = data ; for ( const [ key , value ] of Object . entries ( data ) ) { if ( value == null ) continue ; const newKeyPath = keypath ? ( isCollection ? `${ keypath }[${ key }]` : `${ keypath }.${ key }` ) : ( isCollection ? `[${ key }]` : key ) ; let node = tree . getChild ( key ) ; if ( Array . isArray ( value ) ) { let subtree ; if ( node && node . type === 'tree' ) subtree = node as LocaleTree ; tree . setChild ( key , this . updateTree ( subtree , value , newKeyPath , key , options , true ) ) ; continue ; } if ( isObject ( value ) ) { let subtree ; if ( node && node . type === 'tree' ) subtree = node as LocaleTree ; tree . setChild ( key , this . updateTree ( subtree , value , newKeyPath , key , options ) ) ; continue ; } if ( ! node ) { node = new LocaleNode ( { keypath : newKeyPath , keyname : key , readonly : options . readonly , features : options . features , meta : options . meta , } ) ; tree . setChild ( key , node ) ; this . _flattenLocaleTree [ resolveFlattenRootKeypath ( node . keypath ) ] = node ; } if ( node && node . type === 'node' ) { node . locales [ options . locale ] = new LocaleRecord ( { keypath : newKeyPath , keyname : key , value : `${ value }` , locale : options . locale , filepath : options . filepath , features : options . features , meta : options . meta , readonly : options . readonly , } ) ; } } return tree ; } rewriteKeys ( key , source , context = { } ) { if ( Config . disablePathParsing ) return key ; for ( const framework of Global . enabledFrameworks ) key = framework . rewriteKeys ( key , source , context ) ; return key ; } preprocessData ( data , context = { } ) { for ( const framework of Global . enabledFrameworks ) data = framework . preprocessData ( data , context ) ; return data ; } deprocessData ( data , context = { } ) { for ( const framework of Global . enabledFrameworks ) data = framework . deprocessData ( data , context ) ; return data ; } getTreeNodeByKey ( key , tree ? ) { const root = ! tree ; tree = tree || this . root ; if ( root || Config . disablePathParsing ) { const node = tree . getChild ( key ) ; if ( node ) return node ; } if ( Config . disablePathParsing ) return ; const keys = NodeHelper . splitKeypath ( key ) ; const head = keys [ 0 ] ; const remaining = keys . slice ( 1 ) . join ( '.' ) ; const node = tree . getChild ( head ) ; if ( remaining === '' ) return node ; if ( node && node . type === 'tree' ) return this . getTreeNodeByKey ( remaining , node ) ; return undefined ; } getFilepathByKey ( key , locale ? ) { locale = locale || Config . displayLanguage ; const record = this . getRecordByKey ( key , locale ) ; if ( record && record . filepath ) return record . filepath ; return undefined ; } getNamespaceFromFilepath ( filepath ) { return undefined ; } private stripAnnotationString ( str , maxlength = 0 ) { if ( ! str ) return ; if ( maxlength && str . length > maxlength ) str = `${ str . substring ( 0 , maxlength ) }…` ; return str ; } getValueByKey ( key , locale ? , maxlength = 0 , stringifySpace ? , context = { } ) { locale = locale || Config . displayLanguage ; const node = resolveFlattenRoot ( this . getTreeNodeByKey ( key ) ) ; if ( ! node ) return undefined ; if ( node . type === 'tree' ) { const value = node . values [ locale ] ; if ( ! value ) return undefined ; let text = JSON . stringify ( value , null , stringifySpace ) . replace ( /"(\w+?)":/g , ' $1:' ) . replace ( /}/ , ' }' ) ; if ( maxlength && text . length > maxlength ) { if ( node . isCollection ) text = '[…]' ; else text = '{…}' ; } return text ; } else { return this . stripAnnotationString ( node . getValue ( locale , true ) , maxlength ) ; } } getShadowNodeByKey ( key ) { return new LocaleNode ( { keypath : key , shadow : true } ) ; } getNodeByKey ( key , shadow = false ) { const node = resolveFlattenRoot ( this . getTreeNodeByKey ( key ) ) ; if ( ! node && shadow ) return this . getShadowNodeByKey ( key ) ; if ( node && node . type !== 'tree' ) return node ; } getTranslationsByKey ( key , shadow = true ) { const node = this . getNodeByKey ( key , shadow ) ; if ( ! node ) return { } ; if ( shadow ) return this . getShadowLocales ( node ) ; else return node . locales ; } getRecordByKey ( key , locale , shadow = false ) { const trans = this . getTranslationsByKey ( key , shadow ) ; return trans [ locale ] ; } getShadowLocales ( node , listedLocales ? ) { const locales = { } ; listedLocales = listedLocales || Global . getVisibleLocales ( this . locales ) ; listedLocales . forEach ( ( locale ) => { if ( node . locales [ locale ] ) { locales [ locale ] = node . locales [ locale ] ; } else { locales [ locale ] = new LocaleRecord ( { locale , value : '' , shadow : true , keyname : node . keyname , keypath : node . keypath , meta : node . meta , filepath : undefined , readonly : node . readonly , } ) ; } } ) ; return locales ; } abstract write ( pendings ) ; canHandleWrites ( pending ) { return false ; } searchKeyForTranslations ( text , locale = Config . sourceLanguage ) { return this . keys . find ( i => this . getTranslationsByKey ( i , false ) ?. [ locale ] ?. value === text ) ; } protected onDispose ( ) { this . _disposables . forEach ( d => d . dispose ( ) ) ; this . _disposables = [ ] ; } }