import { I18nParser } from './i18n.parser' ; import { I18N_PARSER_OPTIONS } from '../i18n.constants' ; import { Inject , OnModuleDestroy } from '@nestjs/common' ; import * as path from 'path' ; import * as fs from 'fs' ; import { getDirectories , getFiles } from '../utils/file' ; import { promisify } from 'util' ; import { I18nTranslation } from '../interfaces/i18n-translation.interface' ; import { Observable , Subject , merge as ObservableMerge , from as ObservableFrom , } from 'rxjs' ; import * as chokidar from 'chokidar' ; import { switchMap } from 'rxjs/operators' ; const readFile = promisify ( fs . readFile ) ; const exists = promisify ( fs . exists ) ; export interface I18nJsonParserOptions { path : string ; filePattern ? : string ; watch ? : boolean ; } const defaultOptions = { filePattern : '*.json' , watch : false , } ; export class I18nJsonParser extends I18nParser implements OnModuleDestroy { private watcher ? : chokidar . FSWatcher ; private events : Subject < string > = new Subject ( ) ; constructor ( @ Inject ( I18N_PARSER_OPTIONS ) private options ) { super ( ) ; this . options = this . sanitizeOptions ( options ) ; if ( this . options . watch ) { this . watcher = chokidar . watch ( this . options . path , { ignoreInitial : true } ) . on ( 'all' , ( event ) => { this . events . next ( event ) ; } ) ; } } async onModuleDestroy ( ) { if ( this . watcher ) { await this . watcher . close ( ) ; } } async languages ( ) { if ( this . options . watch ) { return ObservableMerge ( ObservableFrom ( this . parseLanguages ( ) ) , this . events . pipe ( switchMap ( ( ) => this . parseLanguages ( ) ) ) ) ; } return this . parseLanguages ( ) ; } async parse ( ) { if ( this . options . watch ) { return ObservableMerge ( ObservableFrom ( this . parseTranslations ( ) ) , this . events . pipe ( switchMap ( ( ) => this . parseTranslations ( ) ) ) ) ; } return this . parseTranslations ( ) ; } private async parseTranslations ( ) { const i18nPath = path . normalize ( this . options . path + path . sep ) ; const translations = { } ; if ( ! ( await exists ( i18nPath ) ) ) { throw new Error ( `i18n path (${ i18nPath }) cannot be found` ) ; } if ( ! this . options . filePattern . match ( /\*\.[A-z]+/ ) ) { throw new Error ( `filePattern should be formatted like: *.json, *.txt, *.custom etc` ) ; } const languages = await this . parseLanguages ( ) ; const pattern = new RegExp ( '.' + this . options . filePattern . replace ( '.' , '.' ) ) ; const files = await [ ... languages . map ( ( l ) => path . join ( i18nPath , l ) ) , i18nPath , ] . reduce ( async ( f , p ) => { ( await f ) . push ( ... ( await getFiles ( p , pattern ) ) ) ; return f ; } , Promise . resolve ( [ ] ) ) ; for ( const file of files ) { let global = false ; const key = path . dirname ( path . relative ( i18nPath , file ) ) . split ( path . sep ) [ 0 ] ; if ( key === '.' ) { global = true ; } const data = JSON . parse ( await readFile ( file , 'utf8' ) ) ; const prefix = path . basename ( file ) . split ( '.' ) [ 0 ] ; for ( const property of Object . keys ( data ) ) { [ ... ( global ? languages : [ key ] ) ] . forEach ( ( lang ) => { translations [ lang ] = ! ! translations [ lang ] ? translations [ lang ] : { } ; if ( global ) { translations [ lang ] [ property ] = data [ property ] ; } else { translations [ lang ] [ prefix ] = ! ! translations [ lang ] [ prefix ] ? translations [ lang ] [ prefix ] : { } ; translations [ lang ] [ prefix ] [ property ] = data [ property ] ; } } ) ; } } return translations ; } private async parseLanguages ( ) { const i18nPath = path . normalize ( this . options . path + path . sep ) ; return ( await getDirectories ( i18nPath ) ) . map ( ( dir ) => path . relative ( i18nPath , dir ) ) ; } private sanitizeOptions ( options ) { options = { ... defaultOptions , ... options } ; options . path = path . normalize ( options . path + path . sep ) ; if ( ! options . filePattern . startsWith ( '*.' ) ) { options . filePattern = '*.' + options . filePattern ; } return options ; } }