import * as Webpack from 'webpack' ; import * as lodash from 'lodash' ; import { Constants } from './utilities/Constants' ; import { ILocaleElementMap } from './interfaces' ; import { LocalizationPlugin , IStringSerialNumberData as IStringData } from './LocalizationPlugin' ; interface IReconstructionElement { kind : 'static' | 'localized' | 'dynamic' ; } interface IStaticReconstructionElement extends IReconstructionElement { kind : 'static' ; staticString : string ; } interface ILocalizedReconstructionElement extends IReconstructionElement { kind : 'localized' ; values : ILocaleElementMap ; size : number ; stringName : string ; escapedBackslash : string ; locFilePath : string ; } interface IDynamicReconstructionElement extends IReconstructionElement { kind : 'dynamic' ; valueFn : ( locale : string , token : string | undefined ) => string ; size : number ; escapedBackslash : string ; token ? : string ; } interface IParseResult { issues : string [ ] ; reconstructionSeries : IReconstructionElement [ ] ; } interface IReconstructedString { source : string ; size : number ; } interface ILocalizedReconstructionResult { result : Map < string , IReconstructedString > ; issues : string [ ] ; } interface INonLocalizedReconstructionResult { result : IReconstructedString ; issues : string [ ] ; } export interface IProcessAssetOptionsBase { plugin : LocalizationPlugin ; compilation : Webpack . compilation . Compilation ; assetName : string ; asset : IAsset ; chunk : Webpack . compilation . Chunk ; noStringsLocaleName : string ; chunkHasLocalizedModules : ( chunk : Webpack . compilation . Chunk ) => boolean ; } export interface IProcessNonLocalizedAssetOptions extends IProcessAssetOptionsBase { } export interface IProcessLocalizedAssetOptions extends IProcessAssetOptionsBase { locales : Set < string > ; fillMissingTranslationStrings : boolean ; defaultLocale : string ; } export interface IAsset { size ( ) : number ; source ( ) : string ; } export interface IProcessAssetResult { filename : string ; asset : IAsset ; } export const PLACEHOLDER_REGEX = new RegExp ( `${ Constants . STRING_PLACEHOLDER_PREFIX }_(\\\\*)_([A-C])(\\+[^+]+\\+)?_(\\d+)` , 'g' ) ; export class AssetProcessor { public static processLocalizedAsset ( options ) { const assetSource = options . asset . source ( ) ; const parsedAsset = AssetProcessor . _parseStringToReconstructionSequence ( options . plugin , assetSource , this . _getJsonpFunction ( options . chunk , options . chunkHasLocalizedModules , options . noStringsLocaleName ) ) ; const reconstructedAsset = AssetProcessor . _reconstructLocalized ( parsedAsset . reconstructionSeries , options . locales , options . fillMissingTranslationStrings , options . defaultLocale , options . asset . size ( ) ) ; const parsedAssetName = AssetProcessor . _parseStringToReconstructionSequence ( options . plugin , options . assetName , ( ) => { throw new Error ( 'unsupported' ) ; } ) ; const reconstructedAssetName = AssetProcessor . _reconstructLocalized ( parsedAssetName . reconstructionSeries , options . locales , options . fillMissingTranslationStrings , options . defaultLocale , options . assetName . length ) ; const result = new Map < string , IProcessAssetResult > ( ) ; for ( const [ locale , { source , size } ] of reconstructedAsset . result ) { const newAsset = lodash . clone ( options . asset ) ; newAsset . source = ( ) => source ; newAsset . size = ( ) => size ; result . set ( locale , { filename : reconstructedAssetName . result . get ( locale ) ! . source , asset : newAsset } ) ; } const issues = [ ... parsedAsset . issues , ... reconstructedAsset . issues , ... parsedAssetName . issues , ... reconstructedAssetName . issues ] ; if ( issues . length > 0 ) { options . compilation . errors . push ( Error ( `localization:\n${ issues . map ( ( issue ) => `  ${ issue }` ) . join ( '\n' ) }` ) ) ; } return result ; } public static processNonLocalizedAsset ( options ) { const assetSource = options . asset . source ( ) ; const parsedAsset = AssetProcessor . _parseStringToReconstructionSequence ( options . plugin , assetSource , this . _getJsonpFunction ( options . chunk , options . chunkHasLocalizedModules , options . noStringsLocaleName ) ) ; const reconstructedAsset = AssetProcessor . _reconstructNonLocalized ( parsedAsset . reconstructionSeries , options . asset . size ( ) , options . noStringsLocaleName ) ; const parsedAssetName = AssetProcessor . _parseStringToReconstructionSequence ( options . plugin , options . assetName , ( ) => { throw new Error ( 'unsupported' ) ; } ) ; const reconstructedAssetName = AssetProcessor . _reconstructNonLocalized ( parsedAssetName . reconstructionSeries , options . assetName . length , options . noStringsLocaleName ) ; const issues = [ ... parsedAsset . issues , ... reconstructedAsset . issues , ... parsedAssetName . issues , ... reconstructedAssetName . issues ] ; if ( issues . length > 0 ) { options . compilation . errors . push ( Error ( `localization:\n${ issues . map ( ( issue ) => `  ${ issue }` ) . join ( '\n' ) }` ) ) ; } const newAsset = lodash . clone ( options . asset ) ; newAsset . source = ( ) => reconstructedAsset . result . source ; newAsset . size = ( ) => reconstructedAsset . result . size ; return { filename : reconstructedAssetName . result . source , asset : newAsset } ; } private static _reconstructLocalized ( reconstructionSeries , locales , fillMissingTranslationStrings , defaultLocale , initialSize ) { const localizedResults = new Map < string , IReconstructedString > ( ) ; const issues = [ ] ; for ( const locale of locales ) { const reconstruction = [ ] ; let sizeDiff = 0 ; for ( const element of reconstructionSeries ) { switch ( element . kind ) { case 'static' : { reconstruction . push ( ( element as IStaticReconstructionElement ) . staticString ) ; break ; } case 'localized' : { const localizedElement = element as ILocalizedReconstructionElement ; let newValue = localizedElement . values [ locale ] ; if ( ! newValue ) { if ( fillMissingTranslationStrings ) { newValue = localizedElement . values [ defaultLocale ] ; } else { issues . push ( `The string "${ localizedElement . stringName }" in "${ localizedElement . locFilePath }" is missing in ` + `the locale ${ locale }` ) ; newValue = '-- MISSING STRING --' ; } } const escapedBackslash = localizedElement . escapedBackslash || '\\' ; newValue = newValue . replace ( /\\/g , escapedBackslash ) ; const escapingCharacterSequence = escapedBackslash . substr ( escapedBackslash . length / 2 ) ; newValue = newValue . replace ( /\r/g , `${ escapingCharacterSequence }r` ) ; newValue = newValue . replace ( /\n/g , `${ escapingCharacterSequence }n` ) ; newValue = newValue . replace ( /\t/g , `${ escapingCharacterSequence }t` ) ; newValue = newValue . replace ( /\"/g , `${ escapingCharacterSequence }u0022` ) ; newValue = newValue . replace ( /\'/g , `${ escapingCharacterSequence }u0027` ) ; reconstruction . push ( newValue ) ; sizeDiff += newValue . length - localizedElement . size ; break ; } case 'dynamic' : { const dynamicElement = element as IDynamicReconstructionElement ; const newValue = dynamicElement . valueFn ( locale , dynamicElement . token ) ; reconstruction . push ( newValue ) ; sizeDiff += newValue . length - dynamicElement . size ; break ; } } } const newAssetSource = reconstruction . join ( '' ) ; localizedResults . set ( locale , { source : newAssetSource , size : initialSize + sizeDiff } ) ; } return { issues , result : localizedResults } ; } private static _reconstructNonLocalized ( reconstructionSeries , initialSize , noStringsLocaleName ) { const issues = [ ] ; const reconstruction = [ ] ; let sizeDiff = 0 ; for ( const element of reconstructionSeries ) { switch ( element . kind ) { case 'static' : { reconstruction . push ( ( element as IStaticReconstructionElement ) . staticString ) ; break ; } case 'localized' : { const localizedElement = element as ILocalizedReconstructionElement ; issues . push ( `The string "${ localizedElement . stringName }" in "${ localizedElement . locFilePath }" appeared in an asset ` + 'that is not expected to contain localized resources.' ) ; const newValue = '-- NOT EXPECTED TO BE LOCALIZED --' ; reconstruction . push ( newValue ) ; sizeDiff += newValue . length - localizedElement . size ; break ; } case 'dynamic' : { const dynamicElement = element as IDynamicReconstructionElement ; const newValue = dynamicElement . valueFn ( noStringsLocaleName , dynamicElement . token ) ; reconstruction . push ( newValue ) ; sizeDiff += newValue . length - dynamicElement . size ; break ; } } } const newAssetSource = reconstruction . join ( '' ) ; return { issues , result : { source : newAssetSource , size : initialSize + sizeDiff } } ; } private static _parseStringToReconstructionSequence ( plugin , source , jsonpFunction ) { const issues = [ ] ; const reconstructionSeries = [ ] ; let lastIndex = 0 ; let regexResult ; while ( ( regexResult = PLACEHOLDER_REGEX . exec ( source ) ) ) { const staticElement = { kind : 'static' , staticString : source . substring ( lastIndex , regexResult . index ) } ; reconstructionSeries . push ( staticElement ) ; const [ placeholder , escapedBackslash , elementLabel , token , placeholderSerialNumber ] = regexResult ; let localizedReconstructionElement ; switch ( elementLabel ) { case Constants . STRING_PLACEHOLDER_LABEL : { const stringData = plugin . getDataForSerialNumber ( placeholderSerialNumber ) ; if ( ! stringData ) { issues . push ( `Missing placeholder ${ placeholder }` ) ; const brokenLocalizedElement = { kind : 'static' , staticString : placeholder } ; localizedReconstructionElement = brokenLocalizedElement ; } else { const localizedElement = { kind : 'localized' , values : stringData . values , size : placeholder . length , locFilePath : stringData . locFilePath , escapedBackslash : escapedBackslash , stringName : stringData . stringName } ; localizedReconstructionElement = localizedElement ; } break ; } case Constants . LOCALE_NAME_PLACEHOLDER_LABEL : { const dynamicElement = { kind : 'dynamic' , valueFn : ( locale ) => locale , size : placeholder . length , escapedBackslash : escapedBackslash } ; localizedReconstructionElement = dynamicElement ; break ; } case Constants . JSONP_PLACEHOLDER_LABEL : { const dynamicElement = { kind : 'dynamic' , valueFn : jsonpFunction , size : placeholder . length , escapedBackslash : escapedBackslash , token : token . substring ( 1 , token . length - 1 ) } ; localizedReconstructionElement = dynamicElement ; break ; } default : { throw new Error ( `Unexpected label ${ elementLabel }` ) ; } } reconstructionSeries . push ( localizedReconstructionElement ) ; lastIndex = regexResult . index + placeholder . length ; } const lastElement = { kind : 'static' , staticString : source . substr ( lastIndex ) } ; reconstructionSeries . push ( lastElement ) ; return { issues , reconstructionSeries } ; } private static _getJsonpFunction ( chunk , chunkHasLocalizedModules , noStringsLocaleName ) { const idsWithStrings = new Set < number | string > ( ) ; const idsWithoutStrings = new Set < number | string > ( ) ; const asyncChunks = chunk . getAllAsyncChunks ( ) ; for ( const asyncChunk of asyncChunks ) { const chunkId = asyncChunk . id ; if ( chunkId === null || chunkId === undefined ) { throw new Error ( `Chunk "${ asyncChunk . name }"'s ID is null or undefined.` ) ; } if ( chunkHasLocalizedModules ( asyncChunk ) ) { idsWithStrings . add ( chunkId ) ; } else { idsWithoutStrings . add ( chunkId ) ; } } if ( idsWithStrings . size === 0 ) { return ( ) => JSON . stringify ( noStringsLocaleName ) ; } else if ( idsWithoutStrings . size === 0 ) { return ( locale ) => JSON . stringify ( locale ) ; } else { const chunkMapping = { } ; for ( const idWithStrings of idsWithStrings ) { chunkMapping [ idWithStrings ] = 0 ; } for ( const idWithoutStrings of idsWithoutStrings ) { chunkMapping [ idWithoutStrings ] = 1 ; } return ( locale , chunkIdToken ) => { if ( ! locale ) { throw new Error ( 'Missing locale name.' ) ; } return `(${ JSON . stringify ( [ locale , noStringsLocaleName ] ) })[${ JSON . stringify ( chunkMapping ) }[${ chunkIdToken }]]` ; } ; } } }