import path from 'path' ; import YAML from 'js-yaml' ; import fs from 'fs-extra' ; import { EventEmitter , window , workspace , FileSystemWatcher } from 'vscode' ; import { get , set } from 'lodash' ; import { nanoid } from 'nanoid' ; import { cleanObject } from '../utils/cleanObject' ; import { promptEdit } from '../utils/prompts' ; import { FILEWATCHER_TIMEOUT } from '../meta' ; import { Config } from './Config' ; import { ReviewData , ReviewComment , ReviewCommentWithMeta , TranslationCandidate , TranslationCandidateWithMeta , PendingWrite } from './types' ; import { CurrentFile } from './CurrentFile' ; import { Log } from '~/utils' ; export class Reviews { private filepath = '' ; private data : ReviewData = { reviews : { } } ; private _fileWatcher : FileSystemWatcher | undefined ; private mtime = 0 ; private _onDidChange : EventEmitter < string | undefined > = new EventEmitter ( ) ; readonly onDidChange = this . _onDidChange . event ; init ( rootpath ) { this . _fileWatcher ?. dispose ( ) ; this . filepath = path . join ( rootpath , '.vscode/i18n-ally-reviews.yml' ) ; this . _fileWatcher = workspace . createFileSystemWatcher ( this . filepath ) ; this . _fileWatcher . onDidChange ( ( ) => setTimeout ( ( ) => this . load ( ) , FILEWATCHER_TIMEOUT ) ) ; this . _fileWatcher . onDidCreate ( ( ) => setTimeout ( ( ) => this . load ( ) , FILEWATCHER_TIMEOUT ) ) ; this . _fileWatcher . onDidDelete ( ( ) => setTimeout ( ( ) => this . load ( ) , FILEWATCHER_TIMEOUT ) ) ; return this . load ( ) ; } private set ( key , field , value ? , locale ? , save = true ) { if ( locale ) set ( this . data , [ 'reviews' , key , 'locales' , locale , field ] , value ) ; else set ( this . data , [ 'reviews' , key , field ] , value ) ; this . _onDidChange . fire ( key ) ; if ( save ) return this . save ( ) ; } private get ( key , field , locale ? ) { if ( locale ) return get ( this . data , [ 'reviews' , key , 'locales' , locale , field ] ) ; else return get ( this . data , [ 'reviews' , key , field ] ) ; } setDescription ( key , description ? ) { return this . set ( key , 'description' , description ) ; } getDescription ( key ) { return this . get ( key , 'description' ) ; } async setTranslationCandidates ( items ) { for ( const { key , translation , locale } of items ) await this . set ( key , 'translation_candidate' , translation , locale , false ) ; this . save ( ) ; } getTranslationCandidate ( key , locale ) { return this . get ( key , 'translation_candidate' , locale ) ; } async applyTranslationCandidates ( candidates ) { const pendings = [ ] ; for ( const { keypath , locale } of candidates ) { const translation = this . getTranslationCandidate ( keypath , locale ) ; if ( ! translation ) break ; pendings . push ( { keypath , locale , value : translation . text , } ) ; } CurrentFile . loader . write ( pendings ) ; for ( const { keypath , locale } of candidates ) await this . discardTranslationCandidate ( keypath , locale , false ) ; await this . save ( ) ; } async applyTranslationCandidate ( key , locale , override ? ) { const translation = this . getTranslationCandidate ( key , locale ) ; if ( ! translation ) return ; await CurrentFile . loader . write ( { keypath : key , locale , value : override || translation . text , } ) ; await this . discardTranslationCandidate ( key , locale ) ; } async discardTranslationCandidate ( key , locale , save ? ) { await this . set ( key , 'translation_candidate' , undefined , locale , save ) ; } async promptEditTranslation ( key , locale ) { const tc = this . getTranslationCandidate ( key , locale ) ; if ( ! tc ) throw new ReferenceError ( `No translation candidate found for ${ key } on ${ locale }` ) ; let value = tc ?. text ; value = await promptEdit ( key , locale , value ) ; if ( value ) await this . applyTranslationCandidate ( key , locale , value . replace ( /\\n/g , '\n' ) ) ; } getReviews ( key ) { return ( this . data ?. reviews || { } ) [ key ] || { } ; } addComment ( key , locale , comment ) { const comments = this . get ( key , 'comments' , locale ) || [ ] ; comments . push ( { user : Config . reviewUser , id : nanoid ( ) , ... comment , time : new Date ( ) . toISOString ( ) , } ) ; return this . set ( key , 'comments' , comments , locale ) ; } editComment ( key , locale , data ) { const comments = this . get ( key , 'comments' , locale ) || [ ] ; const comment = comments . find ( i => i . id === data . id ) ; if ( comment ) { Object . assign ( comment , data ) ; return this . set ( key , 'comments' , comments , locale ) ; } } getComments ( key , locale , hideResolved = true ) { const comments = this . get ( key , 'comments' , locale ) || [ ] ; if ( hideResolved ) return comments . filter ( i => ! i . resolved ) ; else return comments ; } getCommentById ( key , locale , id ) { const comments = this . getComments ( key , locale , false ) ; const comment = comments . find ( i => i . id === id ) ; return comment ; } getCommentsByLocale ( locale , hideResolved = true ) { return Object . keys ( this . data ?. reviews || { } ) . flatMap ( keypath => this . getComments ( keypath , locale , hideResolved ) . map ( ( i ) => { return { ... i , locale , keypath } as ReviewCommentWithMeta ; } ) ) ; } getTranslationCandidatesLocale ( locale ) { return Object . keys ( this . data ?. reviews || { } ) . map ( ( keypath ) => { const i = this . getTranslationCandidate ( keypath , locale ) ; if ( ! i ) return undefined ; return { ... i , locale , keypath } as TranslationCandidateWithMeta ; } ) . filter ( i => i ) as TranslationCandidateWithMeta [ ] ; } async resolveComment ( key , locale , id ) { const comments = this . getComments ( key , locale , false ) ; const comment = comments . find ( i => i . id === id ) ; if ( comment ) { if ( Config . reviewRemoveCommentOnResolved ) comments . splice ( comments . indexOf ( comment ) , 1 ) ; else comment . resolved = true ; await this . set ( key , 'comments' , comments , locale ) ; return comment ; } } async applySuggestion ( key , locale , id ) { const comment = this . getCommentById ( key , locale , id ) ; if ( comment && comment . suggestion ) { await CurrentFile . loader . write ( { keypath : key , locale , value : comment . suggestion , } ) ; await this . resolveComment ( key , locale , id ) ; } } async promptEditDescription ( keypath ) { const value = await window . showInputBox ( { value : this . getDescription ( keypath ) , prompt : `Description for "${ keypath }"` , ignoreFocusOut : true , } ) ; if ( value !== undefined ) return this . setDescription ( keypath , value ) ; } private async load ( ) { if ( fs . existsSync ( this . filepath ) ) { if ( fs . statSync ( this . filepath ) . mtimeMs === this . mtime ) return ; Log . info ( '📤 Loading review data' ) ; const content = await fs . readFile ( this . filepath , 'utf-8' ) ; this . data = YAML . load ( content ) as any || { reviews : { } } ; } else { this . data = { reviews : { } } ; } this . data . reviews = this . data . reviews || { } ; this . _onDidChange . fire ( undefined ) ; } private async save ( ) { Log . info ( '📥 Saving review data' ) ; this . data = cleanObject ( this . data ) || { } ; let content = YAML . dump ( this . data , { skipInvalid : true } ) ; content = `# Review comments generated by i18n-ally. Please commit this file.\n\n${ content }` ; await fs . writeFile ( this . filepath , content , 'utf-8' ) ; this . mtime = fs . statSync ( this . filepath ) . mtimeMs ; } }