text: /**
   * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
   * For licensing, see LICENSE.md.
   */
  
  'use strict';
  
  const path = require( 'path' );
  const fs = require( 'fs' );
  const findMessages = require( './findmessages' );
  const { EventEmitter } = require( 'events' );
  const PO = require( 'pofile' );
  
  /**
   * A service that serves translations assets based on the found PO files in the registered packages.
   */
  module.exports = class MultipleLanguageTranslationService extends EventEmitter {
  	/**
  	 * @param {Object} options
  	 * @param {String} options.mainLanguage The target language that will be bundled into the main webpack asset.
  	 * @param {Array.<String>} [options.additionalLanguages] Additional languages which files will be emitted.
  	 * When option is set to 'all', all languages found during the compilation will be added.
  	 * @param {Boolean} [options.compileAllLanguages] When set to `true` languages will be found at runtime.
  	 * @param {Boolean} [options.addMainLanguageTranslationsToAllAssets] When set to `true` the service will not complain
  	 * about multiple JS assets and will output translations for the main language to all found assets.
  	 * @param {Boolean} [options.buildAllTranslationsToSeparateFiles] When set to `true` the service will output all translations
  	 * to separate files.
  	 * @param {String|Function|RegExp} [options.translationsOutputFile] An option allowing outputting all translation file
  	 * to the given file. If a file specified by a path (string) does not exist, then it will be created. Otherwise, translations
  	 * will be outputted to the file.
  	 */
  	constructor( {
  		mainLanguage,
  		additionalLanguages = [],
  		compileAllLanguages = false,
  		addMainLanguageTranslationsToAllAssets = false,
  		buildAllTranslationsToSeparateFiles = false,
  		translationsOutputFile,
  		skipPluralFormFunction
  	} ) {
  		super();
  
  		/**
  		 * Main language that should be built in to the bundle.
  		 *
  		 * @private
  		 * @type {String}
  		 */
  		this._mainLanguage = mainLanguage;
  
  		/**
  		 * A set of languages that will be used by translator. This set may be expanded by found languages
  		 * if the `compileAllLanguages` flag is turned on.
  		 *
  		 * @private
  		 * @type {Set.<String>}
  		 */
  		this._languages = new Set( [ mainLanguage, ...additionalLanguages ] );
  
  		/**
  		 * An option indicating if the languages should be found at runtime.
  		 *
  		 * @private
  		 * @type {Boolean}
  		 */
  		this._compileAllLanguages = compileAllLanguages;
  
  		/**
  		 * A boolean option. When set to `true` this service won't complain about multiple JS assets
  		 * and will add translation for the main language to all of them. Useful option for manual tests, etc.
  		 *
  		 * @private
  		 * @type {Boolean}
  		 */
  		this._addMainLanguageTranslationsToAllAssets = addMainLanguageTranslationsToAllAssets;
  
  		/**
  		 * A boolean option. When set to `true` outputs all translations to separate files.
  		 *
  		 * @private
  		 * @type {Boolean}
  		 */
  		this._buildAllTranslationsToSeparateFiles = buildAllTranslationsToSeparateFiles;
  
  		/**
  		 * A set of handled packages that speeds up the translation process.
  		 *
  		 * @private
  		 * @type {Set.<String>}
  		 */
  		this._handledPackages = new Set();
  
  		/**
  		 * A map of translation dictionaries in the `language -> messageId -> single & plural forms` format.
  		 *
  		 * @private
  		 * @type {Object.<String, Object.<String,Array.<String>>>}
  		 */
  		this._translationDictionaries = {};
  
  		/**
  		 * Plural form rules that will be added to generated translation assets.
  		 *
  		 * @private
  		 * @type {Object.<String, String>}
  		 */
  		this._pluralFormsRules = {};
  
  		/**
  		 * A set of message ids that are found in parsed JS files. For each message id a translation
  		 * (with a single and possible plural forms) should be found for the target languages.
  		 *
  		 * @private
  		 * @type {Set.<String>}
  		 */
  		this._foundMessageIds = new Set();
  
  		/**
  		 * Whether the `getPluralForm` function should be added in the bundle file.
  		 *
  		 * @private
  		 * @type {Boolean}
  		 */
  		this._skipPluralFormFunction = skipPluralFormFunction;
  
  		this._translationsOutputFile = translationsOutputFile;
  	}
  
  	/**
  	 * Collects found message ids. Emits a warning when there is a suspicion that the message is created incorrectly
  	 * (e.g. an incorrect `t()` call).
  	 *
  	 * @fires warning
  	 * @param {String} source Content of the source file.
  	 * @param {String} fileName Source file name
  	 * @returns {String}
  	 */
  	translateSource( source, fileName ) {
  		findMessages(
  			source,
  			fileName,
  			message => this.addIdMessage( message.id ),
  			error => this.emit( 'warning', error )
  		);
  
  		return source;
  	}
  
  	/**
  	 * Loads PO files from the package if the package was not registered already.
  	 * If the `compileAllLanguages` flag is set to `true`, then the language set will be expanded to all found languages.
  	 *
  	 * @fires warning
  	 * @param {String} pathToPackage A path to the package containing translations.
  	 */
  	loadPackage( pathToPackage ) {
  		if ( this._handledPackages.has( pathToPackage ) ) {
  			return;
  		}
  
  		this._handledPackages.add( pathToPackage );
  
  		const pathToTranslationDirectory = this._getPathToTranslationDirectory( pathToPackage );
  
  		if ( !fs.existsSync( pathToTranslationDirectory ) ) {
  			return;
  		}
  
  		if ( this._compileAllLanguages ) {
  			for ( const fileName of fs.readdirSync( pathToTranslationDirectory ) ) {
  				if ( !fileName.endsWith( '.po' ) ) {
  					this.emit(
  						'error',
  						`Translation directory (${ pathToTranslationDirectory }) should contain only translation files.`
  					);
  
  					continue;
  				}
  
  				const language = fileName.replace( /\.po$/, '' );
  				const pathToPoFile = path.join( pathToTranslationDirectory, fileName );
  
  				this._languages.add( language );
  				this._loadPoFile( language, pathToPoFile );
  			}
  
  			return;
  		}
  
  		for ( const language of this._languages ) {
  			const pathToPoFile = path.join( pathToTranslationDirectory, language + '.po' );
  
  			this._loadPoFile( language, pathToPoFile );
  		}
  	}
  
  	/**
  	 * Returns an array of partial assets containing translations in the executable JS form.
  	 *
  	 * @fires warning
  	 * @fires error
  	 * @param {Object} options
  	 * @param {String} options.outputDirectory Output directory for the translation files relative to the output.
  	 * @param {Array.<String>} options.compilationAssetNames Original asset names from the compiler (e.g. Webpack).
  	 * @returns {Array.<Object>} Returns new and modified assets that will be added to original ones.
  	 */
  	getAssets( { outputDirectory, compilationAssetNames } ) {
  		let bundledLanguage = this._mainLanguage;
  
  		if ( compilationAssetNames.length === 0 ) {
  			return [];
  		}
  
  		if ( this._translationsOutputFile ) {
  			return this._getAssetsWithTranslationsBundledToTheOutputFile( { outputDirectory, compilationAssetNames } );
  		}
  
  		if ( this._buildAllTranslationsToSeparateFiles ) {
  			bundledLanguage = null;
  			compilationAssetNames = [];
  		} else if ( compilationAssetNames.length > 1 && !this._addMainLanguageTranslationsToAllAssets ) {
  			this.emit( 'error', [
  				'Too many JS assets has been found during the compilation. ' +
  				'You should use one of the following options to specify the strategy:\n' +
  				'- use `addMainLanguageTranslationsToAllAssets` to add translations for the main language to all assets,' +
  				'- use `buildAllTranslationsToSeparateFiles` to add translation files via `<script>` tags in HTML file,' +
  				'- use `translationsOutputFile` to append translation to the existing file or create a new asset.' +
  				'For more details visit https://github.com/ckeditor/ckeditor5-dev/tree/master/packages/ckeditor5-dev-webpack-plugin.'
  			].join( '\n' ) );
  
  			compilationAssetNames = [];
  			bundledLanguage = null;
  		}
  
  		const assetLanguages = Array.from( this._languages )
  			.filter( lang => lang !== bundledLanguage );
  
  		return [
  			// Assets where translations for the main language will be added.
  			...compilationAssetNames.map( assetName => ( {
  				outputBody: this._getTranslationAssets( outputDirectory, [ this._mainLanguage ] )[ 0 ].outputBody,
  				outputPath: assetName,
  				shouldConcat: true
  			} ) ),
  
  			// Translation assets outputted to separate translation files.
  			...this._getTranslationAssets( outputDirectory, assetLanguages )
  		];
  	}
  
  	/**
  	 * Adds the specified `id` to the collection which will be translated to the specified language.
  	 *
  	 * @param {String} id
  	 */
  	addIdMessage( id ) {
  		this._foundMessageIds.add( id );
  	}
  
  	/**
  	 * @param {Object} options
  	 * @param {String} options.outputDirectory Output directory for the translation files relative to the output.
  	 * @param {Array.<String>} options.compilationAssetNames Original asset names from the compiler (e.g. Webpack).
  	 * @returns {Array.<Object>} Returns an array with one asset that
  	 */
  	_getAssetsWithTranslationsBundledToTheOutputFile( { outputDirectory, compilationAssetNames } ) {
  		const assetName = match( this._translationsOutputFile, compilationAssetNames );
  
  		if ( !assetName && typeof this._translationsOutputFile !== 'string' ) {
  			throw new Error( 'No file was matching the `translationsOutputFile` option.' );
  		}
  
  		const translationsBundle = this._getTranslationAssets( outputDirectory, Array.from( this._languages ) )
  			.map( asset => asset.outputBody )
  			.join( '' );
  
  		return [ {
  			outputBody: translationsBundle,
  			outputPath: assetName || this._translationsOutputFile,
  			// Concat with an existing asset if it exists.
  			shouldConcat: compilationAssetNames.some( name => name === assetName )
  		} ];
  	}
  
  	/**
  	 * Returns assets for the given directory and languages.
  	 *
  	 * @private
  	 * @param {String} outputDirectory The output directory for assets.
  	 * @param {Array.<String>} languages Languages for assets.
  	 */
  	_getTranslationAssets( outputDirectory, languages ) {
  		// Sort the array of message ids to provide deterministic results.
  		const sortedMessageIds = Array.from( this._foundMessageIds ).sort( ( a, b ) => a.localeCompare( b ) );
  
  		return languages.map( language => {
  			const outputPath = path.join( outputDirectory, `${ language }.js` );
  
  			if ( !this._translationDictionaries[ language ] ) {
  				this.emit( 'error', `No translation has been found for the ${ language } language.` );
  
  				return { outputBody: '', outputPath };
  			}
  
  			const translations = this._getTranslations( language, sortedMessageIds );
  
  			// Examples of plural forms:
  			// pluralForms="nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2)"
  			// pluralForms="nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2"
  
  			/** @type {String} */
  			const pluralFormsRule = this._pluralFormsRules[ language ];
  
  			let pluralFormFunction;
  
  			// Do not add the `getPluralForm()` function if an integrator disabled it.
  			if ( !this._skipPluralFormFunction ) {
  				if ( !pluralFormsRule ) {
  					// This could be improved in the future by using a 3-rd party library for plural forms.
  					this.emit( 'warning', `The plural form function for the '${ language }' language has not been set.` );
  				} else {
  					const pluralFormFunctionBodyMatch = pluralFormsRule.match( /(?:plural=)(.+)/ );
  
  					// Add support for ES5 - this function will not be transpiled.
  					pluralFormFunction = `function(n){return ${ pluralFormFunctionBodyMatch[ 1 ] };}`;
  				}
  			}
  
  			// Stringify translations and remove unnecessary `""` around property names.
  			const stringifiedTranslations = JSON.stringify( translations )
  				.replace( /"([\w_]+)":/g, '$1:' );
  
  			const outputBody = (
  				'(function(d){' +
  				`	const l = d['${ language }'] = d['${ language }'] || {};` +
  				'	l.dictionary=Object.assign(' +
  				'		l.dictionary||{},' +
  				`		${ stringifiedTranslations }` +
  				'	);' +
  				( pluralFormFunction ? `l.getPluralForm=${ pluralFormFunction };` : '' ) +
  				'})(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));'
  			);
  
  			return { outputBody, outputPath };
  		} );
  	}
  
  	/**
  	 * Walks through the set of found message ids and collects corresponding strings in the target language dictionary.
  	 * Skips messages that lacks their translations.
  	 *
  	 * @private
  	 * @param {String} language The target language
  	 * @param {Array.<String>} sortedMessageIds An array of sorted message ids.
  	 * @returns {Object.<String,String|String[]>}
  	 */
  	_getTranslations( language, sortedMessageIds ) {
  		const langDictionary = this._translationDictionaries[ language ];
  		const translatedStrings = {};
  
  		for ( const messageId of sortedMessageIds ) {
  			const translatedMessage = langDictionary[ messageId ];
  
  			if ( !translatedMessage || translatedMessage.length === 0 ) {
  				this.emit( 'warning', `A translation is missing for '${ messageId }' in the '${ language }' language.` );
  
  				continue;
  			}
  
  			// Register first form as a default form if only one form was provided.
  			translatedStrings[ messageId ] = translatedMessage.length > 1 ?
  				translatedMessage :
  				translatedMessage[ 0 ];
  		}
  
  		return translatedStrings;
  	}
  
  	/**
  	 * Loads translations from the PO file if that file exists.
  	 *
  	 * @private
  	 * @param {String} language PO file's language.
  	 * @param {String} pathToPoFile Path to the target PO file.
  	 */
  	_loadPoFile( language, pathToPoFile ) {
  		if ( !fs.existsSync( pathToPoFile ) ) {
  			return;
  		}
  
  		const parsedTranslationFile = PO.parse( fs.readFileSync( pathToPoFile, 'utf-8' ) );
  
  		this._pluralFormsRules[ language ] = this._pluralFormsRules[ language ] || parsedTranslationFile.headers[ 'Plural-Forms' ];
  
  		if ( !this._translationDictionaries[ language ] ) {
  			this._translationDictionaries[ language ] = {};
  		}
  
  		const dictionary = this._translationDictionaries[ language ];
  
  		for ( const item of parsedTranslationFile.items ) {
  			dictionary[ item.msgid ] = item.msgstr;
  		}
  	}
  
  	/**
  	 * Returns a path to the translation directory depending on the path to the package.
  	 *
  	 * @protected
  	 * @param {String|null} relativePathToPackage
  	 * @returns {String}
  	 */
  	_getPathToTranslationDirectory( relativePathToPackage ) {
  		// If the `relativePathToPackage` is not specified, translations for a single package are processed.
  		if ( relativePathToPackage ) {
  			return path.join( relativePathToPackage, 'lang', 'translations' );
  		}
  
  		return path.join( 'lang', 'translations' );
  	}
  };
  
  /**
   * @param {String|Function|RegExp} predicate
   * @param {Array.<String>} options
   * @returns {String|undefined}
   */
  function match( predicate, options ) {
  	if ( typeof predicate === 'function' ) {
  		return options.find( predicate );
  	}
  
  	if ( typeof predicate === 'string' ) {
  		return options.find( option => predicate === option );
  	}
  
  	if ( predicate instanceof RegExp ) {
  		return options.find( option => predicate.test( option ) );
  	}
  
  	throw new Error(
  		'The CKEditorWebpackPlugin matching function got an unsupported type of a predicate.' +
  		`Got '${ predicate }' (${ typeof predicate } ) where supported values are only 'string', 'regexp' and 'function'.`
  	);
  }
  