From 3128d46cf8cee5dc5f146cfb64d4c1abd2dddf4f Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Thu, 9 Nov 2017 10:46:34 +0100 Subject: [PATCH 01/33] WIP - Translation service v2. --- .../lib/translations/translationservice.js | 71 ++++++++++++++---- .../ckeditor5-dev-webpack-plugin/lib/index.js | 16 ++-- .../lib/serve-translations.js | 73 +++++++++++++++++++ 3 files changed, 140 insertions(+), 20 deletions(-) create mode 100644 packages/ckeditor5-dev-webpack-plugin/lib/serve-translations.js diff --git a/packages/ckeditor5-dev-utils/lib/translations/translationservice.js b/packages/ckeditor5-dev-utils/lib/translations/translationservice.js index b3a5e4fa7..0c2dbaeba 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/translationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/translationservice.js @@ -22,12 +22,33 @@ module.exports = class TranslationService { * @param {Object} [options] Optional config. * @param {Function} [options.getPathToPoFile] Function that return a full path to the po file. */ - constructor( language, options = {} ) { - this.language = language; + constructor( languages, options = {} ) { + /** + * @readonly + */ + this.languages = languages; + + /** + * @readonly + */ this.getPathToPoFile = options.getPathToPoFile || getDefaultPathToPoFile; + /** + * @readonly + */ this.packagePaths = new Set(); - this.dictionary = new Map(); + + /** + * @readonly + */ + this.dictionary = {}; + + /** + * string -> hash dictionary. + * + * @readonly + */ + this.translationHashDictionary = {}; } /** @@ -42,9 +63,11 @@ module.exports = class TranslationService { this.packagePaths.add( pathToPackage ); - const pathToPoFile = this.getPathToPoFile( pathToPackage, this.language ); + for ( const language of this.languages ) { + const pathToPoFile = this.getPathToPoFile( pathToPackage, language ); - this._loadPoFile( pathToPoFile ); + this._loadPoFile( language, pathToPoFile ); + } } /** @@ -79,7 +102,7 @@ module.exports = class TranslationService { } changesInCode = true; - node.arguments[ 0 ].value = this._translateString( node.arguments[ 0 ].value ); + node.arguments[ 0 ].value = this.getHash( node.arguments[ 0 ].value ); } } ); @@ -97,7 +120,7 @@ module.exports = class TranslationService { } // Loads translations from the po file. - _loadPoFile( pathToPoFile ) { + _loadPoFile( language, pathToPoFile ) { if ( !fs.existsSync( pathToPoFile ) ) { return; } @@ -105,22 +128,42 @@ module.exports = class TranslationService { const poFileContent = fs.readFileSync( pathToPoFile, 'utf-8' ); const parsedTranslationFile = createDictionaryFromPoFileContent( poFileContent ); + if ( !this.dictionary[ language ] ) { + this.dictionary[ language ] = {}; + } + + const dictionary = this.dictionary[ language ]; + for ( const translationKey in parsedTranslationFile ) { - this.dictionary.set( translationKey, parsedTranslationFile[ translationKey ] ); + dictionary[ translationKey ] = parsedTranslationFile[ translationKey ]; } } // Translates all t() call found in source text to the target language. - _translateString( originalString ) { - let translation = this.dictionary.get( originalString ); + getHash( originalString ) { + // TODO - log when translation is missing. + + let hash = this.translationHashDictionary[ originalString ]; + + if ( !hash ) { + hash = Math.random().toFixed( 10 ).slice( 2 ); + this.translationHashDictionary[ originalString ] = hash; + } + + return hash; + } + + getHashToTranslatedStringDictionary( lang ) { + const langDictionary = this.dictionary[ lang ]; + const hashes = {}; - if ( !translation ) { - logger.error( `Missing translation for: ${ originalString }.` ); + for ( const stringName in langDictionary ) { + const hash = this.translationHashDictionary[ stringName ]; - translation = originalString; + hashes[ hash ] = langDictionary[ stringName ]; } - return translation; + return hashes; } }; diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/index.js b/packages/ckeditor5-dev-webpack-plugin/lib/index.js index 0620d951c..0d075e98e 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/index.js @@ -5,13 +5,15 @@ 'use strict'; -const replaceTCalls = require( './replacetcalls' ); +// const replaceTCalls = require( './replacetcalls' ); +const serveTranslations = require( './serve-translations' ); module.exports = class CKEditorWebpackPlugin { /** * @param {Object} [options] * @param {Array.} [options.packages] Array of directories in which packages will be looked for. * @param {Array.} [options.languages] Array of languages. + * TODO */ constructor( options = {} ) { this.options = options; @@ -20,10 +22,12 @@ module.exports = class CKEditorWebpackPlugin { apply( compiler ) { const { languages } = this.options; - if ( languages && languages.length == 1 ) { - replaceTCalls( compiler, languages[ 0 ] ); - } else { - throw new Error( 'Multi-language support is not implemented yet.' ); - } + // if ( languages && languages.length == 1 ) { + // replaceTCalls( compiler, languages[ 0 ] ); + // } else { + // throw new Error( 'Multi-language support is not implemented yet.' ); + // } + + serveTranslations( compiler, languages ); } }; diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/serve-translations.js b/packages/ckeditor5-dev-webpack-plugin/lib/serve-translations.js new file mode 100644 index 000000000..3a9117848 --- /dev/null +++ b/packages/ckeditor5-dev-webpack-plugin/lib/serve-translations.js @@ -0,0 +1,73 @@ +const utils = require( './utils' ); +const { TranslationService } = require( '@ckeditor/ckeditor5-dev-utils' ).translations; +const path = require( 'path' ); + +module.exports = function serveTranslations( compiler, languages ) { + const allLanguages = [ languages.main, ...languages.additional ]; + + const translationService = new TranslationService( allLanguages ); + + compiler.options.translateSource = source => translationService.translateSource( source ); + + // Adds ckeditor5-core translations before translate-source-loader starts translating. + compiler.plugin( 'after-resolvers', () => { + compiler.resolvers.normal.resolve( + process.cwd(), + process.cwd(), + '@ckeditor/ckeditor5-core/src/editor/editor.js', + ( err, result ) => { + const pathToCoreTranslationPackage = result.match( utils.CKEditor5CoreRegExp )[ 0 ]; + + translationService.loadPackage( pathToCoreTranslationPackage ); + } + ); + } ); + + compiler.plugin( 'normal-module-factory', nmf => { + nmf.plugin( 'after-resolve', ( resolveOptions, done ) => { + maybeLoadPackage( resolveOptions ); + maybeAddLoader( resolveOptions ); + + done( null, resolveOptions ); + } ); + } ); + + // Adds package to the translations if the resource comes from ckeditor5-* package. + function maybeLoadPackage( resolveOptions ) { + const packageNameRegExp = utils.CKEditor5PackageNameRegExp; + const match = resolveOptions.resource.match( packageNameRegExp ); + + if ( match ) { + const index = resolveOptions.resource.search( packageNameRegExp ) + match[ 0 ].length; + const pathToPackage = resolveOptions.resource.slice( 0, index ); + + translationService.loadPackage( pathToPackage ); + } + } + + // Injects loader when the file comes from ckeditor5-* packages. + function maybeAddLoader( resolveOptions ) { + if ( resolveOptions.resource.match( utils.CKEditor5PackageSrcFileRegExp ) ) { + resolveOptions.loaders.unshift( path.join( __dirname, 'translatesourceloader.js' ) ); + } + } + + compiler.plugin( 'emit', ( compilation, done ) => { + for ( const lang of allLanguages ) { + const hashToTranslatedStringDictionary = translationService.getHashToTranslatedStringDictionary( lang ); + + // TODO: Windows. + const outputPath = path.join( languages.outputDirectory, `${ lang }.json` ); + const output = JSON.stringify( hashToTranslatedStringDictionary, null, 2 ); + + compilation.assets[ outputPath ] = { + source: () => output, + size: () => output.length, + }; + + console.log( `Created ${ outputPath } translation file.` ); + } + + done(); + } ); +}; From 4ffb882dfcd6e497fa7194bd31ce9bb57cca37f0 Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Fri, 10 Nov 2017 17:57:49 +0100 Subject: [PATCH 02/33] WIP - Aligning code to translation service v2. --- .../lib/translations/collect-utils.js | 4 +- .../lib/translations/translationservice.js | 30 +++++++++- .../ckeditor5-dev-webpack-plugin/lib/index.js | 4 +- .../lib/serve-translations.js | 57 ++++++++++++------- 4 files changed, 70 insertions(+), 25 deletions(-) diff --git a/packages/ckeditor5-dev-env/lib/translations/collect-utils.js b/packages/ckeditor5-dev-env/lib/translations/collect-utils.js index d88e21347..9a94e26cc 100644 --- a/packages/ckeditor5-dev-env/lib/translations/collect-utils.js +++ b/packages/ckeditor5-dev-env/lib/translations/collect-utils.js @@ -17,7 +17,7 @@ const corePackageName = 'ckeditor5-core'; const utils = { /** - * Collect translations and returns array of translations. + * Collect translations and return array of translations. * * @returns {Array.} */ @@ -39,7 +39,7 @@ const utils = { }, /** - * Traverse all packages and returns Map of the all founded language contexts informations + * Traverse all packages and return Map of the all founded language contexts information * (file content and file name). * * @returns {Map.} diff --git a/packages/ckeditor5-dev-utils/lib/translations/translationservice.js b/packages/ckeditor5-dev-utils/lib/translations/translationservice.js index 0c2dbaeba..c23565657 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/translationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/translationservice.js @@ -13,6 +13,28 @@ const walk = require( 'acorn/dist/walk' ); const escodegen = require( 'escodegen' ); const logger = require( '../logger' )(); +let idIndex = 0; + +function getNextId() { + let index = idIndex; + let id = ''; + + while ( true ) { + const char = String.fromCharCode( 97 + ( index % 26 ) ); + index = Math.floor( index / 26 ); + + id = char + id; + + if ( index === 0 ) { + break; + } + } + + idIndex++; + + return id; +} + /** * */ @@ -146,7 +168,7 @@ module.exports = class TranslationService { let hash = this.translationHashDictionary[ originalString ]; if ( !hash ) { - hash = Math.random().toFixed( 10 ).slice( 2 ); + hash = getNextId(); this.translationHashDictionary[ originalString ] = hash; } @@ -160,6 +182,12 @@ module.exports = class TranslationService { for ( const stringName in langDictionary ) { const hash = this.translationHashDictionary[ stringName ]; + if ( !hash ) { + console.error( `Missing translation for ${ stringName } for ${ lang } language.` ); + + continue; + } + hashes[ hash ] = langDictionary[ stringName ]; } diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/index.js b/packages/ckeditor5-dev-webpack-plugin/lib/index.js index 0d075e98e..e00521ee9 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/index.js @@ -12,8 +12,8 @@ module.exports = class CKEditorWebpackPlugin { /** * @param {Object} [options] * @param {Array.} [options.packages] Array of directories in which packages will be looked for. - * @param {Array.} [options.languages] Array of languages. - * TODO + * @param {Object} [options.languages] + * TODO: Fix params. */ constructor( options = {} ) { this.options = options; diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/serve-translations.js b/packages/ckeditor5-dev-webpack-plugin/lib/serve-translations.js index 3a9117848..732185387 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/serve-translations.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/serve-translations.js @@ -1,15 +1,19 @@ const utils = require( './utils' ); -const { TranslationService } = require( '@ckeditor/ckeditor5-dev-utils' ).translations; +const { TranslationService } = require( '@ckeditor/ckeditor5-dev-utils' ).translations; const path = require( 'path' ); +/** + * Serve translations for multiple languages. + * + * @param {*} compiler Webpack compiler + * @param {*} languages + */ module.exports = function serveTranslations( compiler, languages ) { - const allLanguages = [ languages.main, ...languages.additional ]; - - const translationService = new TranslationService( allLanguages ); + const translationService = new TranslationService( languages ); compiler.options.translateSource = source => translationService.translateSource( source ); - // Adds ckeditor5-core translations before translate-source-loader starts translating. + // Add ckeditor5-core translations before translate-source-loader starts translating. compiler.plugin( 'after-resolvers', () => { compiler.resolvers.normal.resolve( process.cwd(), @@ -32,7 +36,7 @@ module.exports = function serveTranslations( compiler, languages ) { } ); } ); - // Adds package to the translations if the resource comes from ckeditor5-* package. + // Add package to the translations if the resource comes from ckeditor5-* package. function maybeLoadPackage( resolveOptions ) { const packageNameRegExp = utils.CKEditor5PackageNameRegExp; const match = resolveOptions.resource.match( packageNameRegExp ); @@ -45,29 +49,42 @@ module.exports = function serveTranslations( compiler, languages ) { } } - // Injects loader when the file comes from ckeditor5-* packages. + // Inject loader when the file comes from ckeditor5-* packages. function maybeAddLoader( resolveOptions ) { if ( resolveOptions.resource.match( utils.CKEditor5PackageSrcFileRegExp ) ) { resolveOptions.loaders.unshift( path.join( __dirname, 'translatesourceloader.js' ) ); } } - compiler.plugin( 'emit', ( compilation, done ) => { - for ( const lang of allLanguages ) { - const hashToTranslatedStringDictionary = translationService.getHashToTranslatedStringDictionary( lang ); + compiler.plugin( 'compilation', compilation => { + compilation.plugin( 'additional-assets', done => { + for ( const lang of languages ) { + const hashToTranslatedStringDictionary = translationService.getHashToTranslatedStringDictionary( lang ); - // TODO: Windows. - const outputPath = path.join( languages.outputDirectory, `${ lang }.json` ); - const output = JSON.stringify( hashToTranslatedStringDictionary, null, 2 ); + // TODO: Windows. + const outputPath = path.join( 'lang', `${ lang }.js` ); + const stringifiedTranslations = JSON.stringify( hashToTranslatedStringDictionary, null, 2 ); + const outputBody = `CKEDITOR_TRANSLATIONS.add( '${ lang }', ${ stringifiedTranslations } )`; - compilation.assets[ outputPath ] = { - source: () => output, - size: () => output.length, - }; + compilation.assets[ outputPath ] = { + source: () => outputBody, + size: () => outputBody.length, + }; - console.log( `Created ${ outputPath } translation file.` ); - } + console.log( `Created ${ outputPath } translation file.` ); + } - done(); + done(); + } ); } ); + + // compiler.plugin( 'emit', compilation => { + // for ( const chunk of compilation.chunks ) { + // const resources = chunk.modules.map( m => m.resource ); + + // console.log( chunk.files, resources ); + // } + + // process.exit(); + // } ); }; From 232fc2bc1732d8aadcea928b47db4339637d9d9d Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Tue, 14 Nov 2017 19:59:34 +0100 Subject: [PATCH 03/33] Added `SingleLanguageTranslationService` Improved API docs, tests. --- .../lib/translations/index.js | 3 +- .../multiplelanguagetranslationservice.js | 162 ++++++++++++++ .../lib/translations/shortidgenerator.js | 37 ++++ .../singlelanguagetranslationservice.js | 85 ++++++++ .../lib/translations/translatesource.js | 61 ++++++ .../lib/translations/translationservice.js | 200 ------------------ .../tests/translations/shortidgenerator.js | 51 +++++ .../tests/translations/translationservice.js | 3 +- .../ckeditor5-dev-webpack-plugin/lib/index.js | 27 ++- .../lib/serve-translations.js | 70 +++--- 10 files changed, 454 insertions(+), 245 deletions(-) create mode 100644 packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js create mode 100644 packages/ckeditor5-dev-utils/lib/translations/shortidgenerator.js create mode 100644 packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js create mode 100644 packages/ckeditor5-dev-utils/lib/translations/translatesource.js delete mode 100644 packages/ckeditor5-dev-utils/lib/translations/translationservice.js create mode 100644 packages/ckeditor5-dev-utils/tests/translations/shortidgenerator.js diff --git a/packages/ckeditor5-dev-utils/lib/translations/index.js b/packages/ckeditor5-dev-utils/lib/translations/index.js index fc9aefb46..fda8b5ee3 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/index.js +++ b/packages/ckeditor5-dev-utils/lib/translations/index.js @@ -6,7 +6,8 @@ 'use strict'; module.exports = { - TranslationService: require( './translationservice' ), + MultipleLanguageTranslationService: require( './multiplelanguagetranslationservice' ), + SingleLanguageTranslationService: require( './singlelanguagetranslationservice' ), findOriginalStrings: require( './findoriginalstrings' ), createDictionaryFromPoFileContent: require( './createdictionaryfrompofilecontent' ), cleanPoFileContent: require( './cleanpofilecontent' ), diff --git a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js new file mode 100644 index 000000000..464fb3915 --- /dev/null +++ b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js @@ -0,0 +1,162 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +const path = require( 'path' ); +const fs = require( 'fs' ); +const createDictionaryFromPoFileContent = require( './createdictionaryfrompofilecontent' ); +const translateSource = require( './translateSource' ); +const ShortIdGenerator = require( './shortidgenerator' ); + +/** + * `MultipleLanguageTranslationService` replaces `t()` call params with short ids + * and provides assets that translate those ids to target languages. + */ +module.exports = class MultipleLanguageTranslationService { + /** + * @param {String} language Target language. + */ + constructor( languages ) { + /** + * @readonly + */ + this.languages = languages; + + /** + * @readonly + */ + this.packagePaths = new Set(); + + /** + * @readonly + */ + this.dictionary = {}; + + /** + * Original string -> hash dictionary gathered from files parsed by loader. + * + * @type {Object.} + * @readonly + */ + this.translationIdsDictionary = {}; + + this._idCreator = new ShortIdGenerator(); + } + + /** + * Translates file's source and replace `t()` call strings with short ids. + * + * @param {String} source + */ + translateSource( source ) { + return translateSource( source, originalString => this._getId( originalString ) ); + } + + /** + * Loads package and tries to get the po file from the package. + * + * @param {String} pathToPackage Path to the package containing translations. + */ + loadPackage( pathToPackage ) { + if ( this.packagePaths.has( pathToPackage ) ) { + return; + } + + this.packagePaths.add( pathToPackage ); + + for ( const language of this.languages ) { + const pathToPoFile = this._getPathToPoFile( pathToPackage, language ); + + this._loadPoFile( language, pathToPoFile ); + } + } + + /** + * Returns an array of assets based on the stored dictionaries + * + * @param {Object} param0 + * @param {String} [param0.outputDirectory] + * @returns {Array.} + */ + getAssets( { outputDirectory = 'lang' } ) { + return this.languages.map( language => { + const { translatedStrings, errors } = this._getIdToTranslatedStringDictionary( language ); + + // TODO: Windows paths. + const outputPath = path.join( outputDirectory, `${ language }.js` ); + const stringifiedTranslations = JSON.stringify( translatedStrings, null, 2 ); + const outputBody = `CKEDITOR_TRANSLATIONS.add( '${ language }', ${ stringifiedTranslations } )`; + + return { outputPath, outputBody, errors }; + } ); + } + + // Walk through the `translationIdsDictionary` and find corresponding strings in target language's dictionary. + // Use original strings if translated ones are missing. + _getIdToTranslatedStringDictionary( lang ) { + const langDictionary = this.dictionary[ lang ]; + const translatedStrings = {}; + const errors = []; + + for ( const originalString in this.translationIdsDictionary ) { + const id = this.translationIdsDictionary[ originalString ]; + const translatedString = langDictionary[ originalString ]; + + if ( !translatedString ) { + errors.push( `Missing translation for ${ originalString } for ${ lang } language.` ); + } + + translatedStrings[ id ] = translatedString || originalString; + } + + return { + translatedStrings, + errors + }; + } + + // Loads translations from the po file. + _loadPoFile( language, pathToPoFile ) { + if ( !fs.existsSync( pathToPoFile ) ) { + return; + } + + const poFileContent = fs.readFileSync( pathToPoFile, 'utf-8' ); + const parsedTranslationFile = createDictionaryFromPoFileContent( poFileContent ); + + if ( !this.dictionary[ language ] ) { + this.dictionary[ language ] = {}; + } + + const dictionary = this.dictionary[ language ]; + + for ( const translationKey in parsedTranslationFile ) { + // TODO: ensure that translation files can't use the same translationKey. + dictionary[ translationKey ] = parsedTranslationFile[ translationKey ]; + } + } + + // Translates all t() call found in source text to the target language. + _getId( originalString ) { + // TODO - log when translation is missing. + + let id = this.translationIdsDictionary[ originalString ]; + + if ( !id ) { + id = this._idCreator.getNextId(); + this.translationIdsDictionary[ originalString ] = id; + } + + return id; + } + + /** + * @protected + */ + _getPathToPoFile( pathToPackage, languageCode ) { + return path.join( pathToPackage, 'lang', 'translations', languageCode + '.po' ); + } +}; diff --git a/packages/ckeditor5-dev-utils/lib/translations/shortidgenerator.js b/packages/ckeditor5-dev-utils/lib/translations/shortidgenerator.js new file mode 100644 index 000000000..43fbefaed --- /dev/null +++ b/packages/ckeditor5-dev-utils/lib/translations/shortidgenerator.js @@ -0,0 +1,37 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +/** + * Generates short sequential ids in [a-z] range. + * a, b, c, ..., z, aa, ab, ... + */ +module.exports = class ShortIdGenerator { + constructor() { + this._idNumber = 0; + } + + getNextId() { + let number = this._idNumber; + const chars = []; + + while ( true ) { + const char = String.fromCharCode( 97 + ( number % 26 ) ); + + chars.unshift( char ); + + if ( number < 26 ) { + break; + } + + number = Math.floor( number / 26 ) - 1; + } + + this._idNumber++; + + return chars.join( '' ); + } +}; diff --git a/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js new file mode 100644 index 000000000..59cd77a91 --- /dev/null +++ b/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js @@ -0,0 +1,85 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +const translateSource = require( './translateSource' ); +const path = require( 'path' ); +const fs = require( 'fs' ); +const createDictionaryFromPoFileContent = require( './createdictionaryfrompofilecontent' ); + +/** + * `SingleLanguageTranslationService` replaces `t()` call strings with translated one directly in the build + */ +module.exports = class SingleLanguageTranslationService { + constructor( languages ) { + /** + * @readonly + */ + this.dictionary = {}; + + /** + * @readonly + */ + this.packagePaths = new Set(); + + this.language = languages[ 0 ]; + } + + /** + * Translates file's source and replace `t()` call strings with translated strings. + * + * @param {String} source + */ + translateSource( source ) { + return translateSource( source, originalString => this.translateString( originalString ) ); + } + + /** + * Loads package and tries to get the po file from the package. + * + * @param {String} pathToPackage Path to the package containing translations. + */ + loadPackage( pathToPackage ) { + if ( this.packagePaths.has( pathToPackage ) ) { + return; + } + + this.packagePaths.add( pathToPackage ); + + const pathToPoFile = this._getPathToPoFile( pathToPackage, this.language ); + + this._loadPoFile( pathToPoFile ); + } + + getAssets() { + return []; + } + + _loadPoFile( pathToPoFile ) { + if ( !fs.existsSync( pathToPoFile ) ) { + return; + } + + const poFileContent = fs.readFileSync( pathToPoFile, 'utf-8' ); + const parsedTranslationFile = createDictionaryFromPoFileContent( poFileContent ); + + for ( const translationKey in parsedTranslationFile ) { + // TODO: ensure that translation files can't use the same translationKey. + this.dictionary[ translationKey ] = parsedTranslationFile[ translationKey ]; + } + } + + translateString( originalString ) { + return this.dictionary[ originalString ]; + } + + /** + * @protected + */ + _getPathToPoFile( pathToPackage, languageCode ) { + return path.join( pathToPackage, 'lang', 'translations', languageCode + '.po' ); + } +}; diff --git a/packages/ckeditor5-dev-utils/lib/translations/translatesource.js b/packages/ckeditor5-dev-utils/lib/translations/translatesource.js new file mode 100644 index 000000000..1bc30e8ea --- /dev/null +++ b/packages/ckeditor5-dev-utils/lib/translations/translatesource.js @@ -0,0 +1,61 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +const acorn = require( 'acorn' ); +const walk = require( 'acorn/dist/walk' ); +const escodegen = require( 'escodegen' ); +const logger = require( '../logger' )(); + +/** + * Parses source, translates `t()` call arguments and returns modified output. + * + * @param {String} source JS source text which will be translated. + * @param {Function} translateString Function that will translate matched string to the destination language or hash. + * @returns {String} Transformed source. + */ +module.exports = function translateSource( source, translateString ) { + const comments = []; + const tokens = []; + + const ast = acorn.parse( source, { + sourceType: 'module', + ranges: true, + onComment: comments, + onToken: tokens + } ); + + let changesInCode = false; + + walk.simple( ast, { + CallExpression: node => { + if ( node.callee.name !== 't' ) { + return; + } + + if ( node.arguments[ 0 ].type !== 'Literal' ) { + logger.error( 'First t() call argument should be a string literal.' ); + + return; + } + + changesInCode = true; + node.arguments[ 0 ].value = translateString( node.arguments[ 0 ].value ); + } + } ); + + // Optimization for files without t() calls. + if ( !changesInCode ) { + return source; + } + + escodegen.attachComments( ast, comments, tokens ); + const output = escodegen.generate( ast, { + comment: true + } ); + + return output; +}; diff --git a/packages/ckeditor5-dev-utils/lib/translations/translationservice.js b/packages/ckeditor5-dev-utils/lib/translations/translationservice.js deleted file mode 100644 index c23565657..000000000 --- a/packages/ckeditor5-dev-utils/lib/translations/translationservice.js +++ /dev/null @@ -1,200 +0,0 @@ -/** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const path = require( 'path' ); -const fs = require( 'fs' ); -const createDictionaryFromPoFileContent = require( './createdictionaryfrompofilecontent' ); -const acorn = require( 'acorn' ); -const walk = require( 'acorn/dist/walk' ); -const escodegen = require( 'escodegen' ); -const logger = require( '../logger' )(); - -let idIndex = 0; - -function getNextId() { - let index = idIndex; - let id = ''; - - while ( true ) { - const char = String.fromCharCode( 97 + ( index % 26 ) ); - index = Math.floor( index / 26 ); - - id = char + id; - - if ( index === 0 ) { - break; - } - } - - idIndex++; - - return id; -} - -/** - * - */ -module.exports = class TranslationService { - /** - * @param {String} language Target language. - * @param {Object} [options] Optional config. - * @param {Function} [options.getPathToPoFile] Function that return a full path to the po file. - */ - constructor( languages, options = {} ) { - /** - * @readonly - */ - this.languages = languages; - - /** - * @readonly - */ - this.getPathToPoFile = options.getPathToPoFile || getDefaultPathToPoFile; - - /** - * @readonly - */ - this.packagePaths = new Set(); - - /** - * @readonly - */ - this.dictionary = {}; - - /** - * string -> hash dictionary. - * - * @readonly - */ - this.translationHashDictionary = {}; - } - - /** - * Loads package and tries to get the po file from the package. - * - * @param {String} pathToPackage Path to the package containing translations. - */ - loadPackage( pathToPackage ) { - if ( this.packagePaths.has( pathToPackage ) ) { - return; - } - - this.packagePaths.add( pathToPackage ); - - for ( const language of this.languages ) { - const pathToPoFile = this.getPathToPoFile( pathToPackage, language ); - - this._loadPoFile( language, pathToPoFile ); - } - } - - /** - * Parses source, translates `t()` call arguments and returns modified output. - * - * @param {String} source JS source text which will be translated. - * @returns {String} - */ - translateSource( source ) { - const comments = []; - const tokens = []; - - const ast = acorn.parse( source, { - sourceType: 'module', - ranges: true, - onComment: comments, - onToken: tokens - } ); - - let changesInCode = false; - - walk.simple( ast, { - CallExpression: node => { - if ( node.callee.name !== 't' ) { - return; - } - - if ( node.arguments[ 0 ].type !== 'Literal' ) { - logger.error( 'First t() call argument should be a string literal.' ); - - return; - } - - changesInCode = true; - node.arguments[ 0 ].value = this.getHash( node.arguments[ 0 ].value ); - } - } ); - - // Optimization for files without t() calls. - if ( !changesInCode ) { - return source; - } - - escodegen.attachComments( ast, comments, tokens ); - const output = escodegen.generate( ast, { - comment: true - } ); - - return output; - } - - // Loads translations from the po file. - _loadPoFile( language, pathToPoFile ) { - if ( !fs.existsSync( pathToPoFile ) ) { - return; - } - - const poFileContent = fs.readFileSync( pathToPoFile, 'utf-8' ); - const parsedTranslationFile = createDictionaryFromPoFileContent( poFileContent ); - - if ( !this.dictionary[ language ] ) { - this.dictionary[ language ] = {}; - } - - const dictionary = this.dictionary[ language ]; - - for ( const translationKey in parsedTranslationFile ) { - dictionary[ translationKey ] = parsedTranslationFile[ translationKey ]; - } - } - - // Translates all t() call found in source text to the target language. - getHash( originalString ) { - // TODO - log when translation is missing. - - let hash = this.translationHashDictionary[ originalString ]; - - if ( !hash ) { - hash = getNextId(); - this.translationHashDictionary[ originalString ] = hash; - } - - return hash; - } - - getHashToTranslatedStringDictionary( lang ) { - const langDictionary = this.dictionary[ lang ]; - const hashes = {}; - - for ( const stringName in langDictionary ) { - const hash = this.translationHashDictionary[ stringName ]; - - if ( !hash ) { - console.error( `Missing translation for ${ stringName } for ${ lang } language.` ); - - continue; - } - - hashes[ hash ] = langDictionary[ stringName ]; - } - - return hashes; - } -}; - -function getDefaultPathToPoFile( pathToPackage, languageCode ) { - return path.join( pathToPackage, 'lang', 'translations', languageCode + '.po' ); -} diff --git a/packages/ckeditor5-dev-utils/tests/translations/shortidgenerator.js b/packages/ckeditor5-dev-utils/tests/translations/shortidgenerator.js new file mode 100644 index 000000000..878af38cf --- /dev/null +++ b/packages/ckeditor5-dev-utils/tests/translations/shortidgenerator.js @@ -0,0 +1,51 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +const { expect } = require( 'chai' ); +const ShortIdGenerator = require( '../../lib/translations/shortidgenerator' ); +const { times } = require( 'lodash' ); + +describe( 'ShortIdCreator', () => { + describe( 'getNextId()', () => { + it( 'should generate id\'s from `a`', () => { + const shortIdGenerator = new ShortIdGenerator(); + + const id = shortIdGenerator.getNextId(); + + expect( id ).to.equal( 'a' ); + } ); + + it( 'should generate sequential id\'s from `a`', () => { + const shortIdGenerator = new ShortIdGenerator(); + + const firstTenIds = times( 10, () => shortIdGenerator.getNextId() ); + + expect( firstTenIds ).to.deep.equal( [ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j' + ] ); + } ); + + it( 'should generate `ab` after `aa` after `z`', () => { + const shortIdGenerator = new ShortIdGenerator(); + + times( 25, () => shortIdGenerator.getNextId() ); + + expect( shortIdGenerator.getNextId() ).to.equal( 'z' ); // 26th. + expect( shortIdGenerator.getNextId() ).to.equal( 'aa' ); // 27th. + expect( shortIdGenerator.getNextId() ).to.equal( 'ab' ); // 28th. + } ); + + it( 'should generate `ba` after `az`', () => { + const shortIdGenerator = new ShortIdGenerator(); + + times( 51, () => shortIdGenerator.getNextId() ); + + expect( shortIdGenerator.getNextId() ).to.equal( 'az' ); // 52th. + expect( shortIdGenerator.getNextId() ).to.equal( 'ba' ); // 53th. + } ); + } ); +} ); diff --git a/packages/ckeditor5-dev-utils/tests/translations/translationservice.js b/packages/ckeditor5-dev-utils/tests/translations/translationservice.js index 06972a069..84da00a4f 100644 --- a/packages/ckeditor5-dev-utils/tests/translations/translationservice.js +++ b/packages/ckeditor5-dev-utils/tests/translations/translationservice.js @@ -5,9 +5,8 @@ 'use strict'; -const chai = require( 'chai' ); +const { expect } = require( 'chai' ); const sinon = require( 'sinon' ); -const expect = chai.expect; const path = require( 'path' ); const proxyquire = require( 'proxyquire' ); diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/index.js b/packages/ckeditor5-dev-webpack-plugin/lib/index.js index e00521ee9..fa72ef88a 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/index.js @@ -5,8 +5,11 @@ 'use strict'; -// const replaceTCalls = require( './replacetcalls' ); const serveTranslations = require( './serve-translations' ); +const { + MultipleLanguageTranslationService, + SingleLanguageTranslationService +} = require( '@ckeditor/ckeditor5-dev-utils' ).translations; module.exports = class CKEditorWebpackPlugin { /** @@ -20,14 +23,22 @@ module.exports = class CKEditorWebpackPlugin { } apply( compiler ) { - const { languages } = this.options; + if ( !this.options.languages ) { + return; + } - // if ( languages && languages.length == 1 ) { - // replaceTCalls( compiler, languages[ 0 ] ); - // } else { - // throw new Error( 'Multi-language support is not implemented yet.' ); - // } + let translationService; - serveTranslations( compiler, languages ); + if ( this.options.optimizeBuildForOneLanguage ) { + if ( this.options.languages.length > 1 ) { + throw new Error( 'Only one language should be specified when `optimizeBuildForOneLanguage` option is on.' ); + } + + translationService = new SingleLanguageTranslationService( this.options.languages[ 0 ] ); + } else { + translationService = new MultipleLanguageTranslationService( this.options.languages ); + } + + serveTranslations( compiler, this.options, translationService ); } }; diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/serve-translations.js b/packages/ckeditor5-dev-webpack-plugin/lib/serve-translations.js index 732185387..bccfcd730 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/serve-translations.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/serve-translations.js @@ -1,19 +1,24 @@ const utils = require( './utils' ); -const { TranslationService } = require( '@ckeditor/ckeditor5-dev-utils' ).translations; const path = require( 'path' ); +const flatten = require( 'lodash/flatten' ); +const chalk = require( 'chalk' ); /** - * Serve translations for multiple languages. + * Serve translations depending on the used translation service and passed options. * * @param {*} compiler Webpack compiler - * @param {*} languages + * @param {Object} options Translation options + * @param {String} options.languages Target languages + * @param {Boolean} [options.throwErrorOnMissingTranslation] Throw when the translation is missing. + * By default original (english strings) are used when the target translation is missing. + * @param {String} [options.outputDirectory='lang'] Output directory for the emitted translation files. + * @param {TranslationService} translationService */ -module.exports = function serveTranslations( compiler, languages ) { - const translationService = new TranslationService( languages ); - +module.exports = function serveTranslations( compiler, options, translationService ) { + // Provides translateSource method for the `translatesourceloader` loader. compiler.options.translateSource = source => translationService.translateSource( source ); - // Add ckeditor5-core translations before translate-source-loader starts translating. + // Add ckeditor5-core translations before `translatesourceloader` starts translating. compiler.plugin( 'after-resolvers', () => { compiler.resolvers.normal.resolve( process.cwd(), @@ -36,6 +41,14 @@ module.exports = function serveTranslations( compiler, languages ) { } ); } ); + compiler.plugin( 'compilation', compilation => { + compilation.plugin( 'additional-assets', done => { + addAssetsToExistingOnes( compilation.assets ); + + done(); + } ); + } ); + // Add package to the translations if the resource comes from ckeditor5-* package. function maybeLoadPackage( resolveOptions ) { const packageNameRegExp = utils.CKEditor5PackageNameRegExp; @@ -56,35 +69,24 @@ module.exports = function serveTranslations( compiler, languages ) { } } - compiler.plugin( 'compilation', compilation => { - compilation.plugin( 'additional-assets', done => { - for ( const lang of languages ) { - const hashToTranslatedStringDictionary = translationService.getHashToTranslatedStringDictionary( lang ); - - // TODO: Windows. - const outputPath = path.join( 'lang', `${ lang }.js` ); - const stringifiedTranslations = JSON.stringify( hashToTranslatedStringDictionary, null, 2 ); - const outputBody = `CKEDITOR_TRANSLATIONS.add( '${ lang }', ${ stringifiedTranslations } )`; + function addAssetsToExistingOnes( destinationAssets ) { + const generatedAssets = translationService.getAssets( { outputDirectory: options.outputDirectory } ); - compilation.assets[ outputPath ] = { - source: () => outputBody, - size: () => outputBody.length, - }; + const errors = flatten( generatedAssets.map( asset => asset.errors ) ); - console.log( `Created ${ outputPath } translation file.` ); - } - - done(); - } ); - } ); - - // compiler.plugin( 'emit', compilation => { - // for ( const chunk of compilation.chunks ) { - // const resources = chunk.modules.map( m => m.resource ); + if ( errors.length && options.throwErrorOnMissingTranslation ) { + throw new Error( errors.join( '\n' ) + '\n' ); + } - // console.log( chunk.files, resources ); - // } + for ( const error of errors ) { + console.error( chalk.red( error ) ); + } - // process.exit(); - // } ); + for ( const asset of generatedAssets ) { + destinationAssets[ asset.outputPath ] = { + source: () => asset.outputBody, + size: () => asset.outputBody.length, + }; + } + } }; From 873bea9164533be8392f82f95e1a7fdb05f96071 Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Wed, 15 Nov 2017 14:02:59 +0100 Subject: [PATCH 04/33] Added TranslationService interface. Improved docs, serveTranslations and translation services. --- .../multiplelanguagetranslationservice.js | 106 +++++++++--------- .../singlelanguagetranslationservice.js | 55 +++++---- .../ckeditor5-dev-webpack-plugin/lib/index.js | 29 +++-- .../lib/serve-translations.js | 102 ++++++++++++----- 4 files changed, 177 insertions(+), 115 deletions(-) diff --git a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js index 464fb3915..a433dfa24 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js @@ -10,64 +10,57 @@ const fs = require( 'fs' ); const createDictionaryFromPoFileContent = require( './createdictionaryfrompofilecontent' ); const translateSource = require( './translateSource' ); const ShortIdGenerator = require( './shortidgenerator' ); +const { EventEmitter } = require( 'events' ); /** * `MultipleLanguageTranslationService` replaces `t()` call params with short ids * and provides assets that translate those ids to target languages. + * + * translationKey - original english string that occur in `t()` call params. */ -module.exports = class MultipleLanguageTranslationService { +module.exports = class MultipleLanguageTranslationService extends EventEmitter { /** - * @param {String} language Target language. + * @param {Array.} languages Target languages. */ constructor( languages ) { - /** - * @readonly - */ - this.languages = languages; - - /** - * @readonly - */ - this.packagePaths = new Set(); - - /** - * @readonly - */ - this.dictionary = {}; - - /** - * Original string -> hash dictionary gathered from files parsed by loader. - * - * @type {Object.} - * @readonly - */ - this.translationIdsDictionary = {}; + super(); + + this._languages = languages; + + this._packagePaths = new Set(); + + // language -> translationKey -> targetTranslation dictionary. + this._dictionary = {}; + + // translationKey -> id dictionary gathered from files parsed by loader. + // @type {Object.} + this._translationIdsDictionary = {}; this._idCreator = new ShortIdGenerator(); } /** - * Translates file's source and replace `t()` call strings with short ids. + * Translate file's source and replace `t()` call strings with short ids. * * @param {String} source */ translateSource( source ) { - return translateSource( source, originalString => this._getId( originalString ) ); + return translateSource( source, translationKey => this._getId( translationKey ) ); } /** - * Loads package and tries to get the po file from the package. + * Load package and tries to get the po file from the package. * * @param {String} pathToPackage Path to the package containing translations. */ loadPackage( pathToPackage ) { - if ( this.packagePaths.has( pathToPackage ) ) { + if ( this._packagePaths.has( pathToPackage ) ) { return; } - this.packagePaths.add( pathToPackage ); + this._packagePaths.add( pathToPackage ); - for ( const language of this.languages ) { + for ( const language of this._languages ) { const pathToPoFile = this._getPathToPoFile( pathToPackage, language ); this._loadPoFile( language, pathToPoFile ); @@ -75,50 +68,55 @@ module.exports = class MultipleLanguageTranslationService { } /** - * Returns an array of assets based on the stored dictionaries + * Return an array of assets based on the stored dictionaries. * * @param {Object} param0 * @param {String} [param0.outputDirectory] * @returns {Array.} */ getAssets( { outputDirectory = 'lang' } ) { - return this.languages.map( language => { - const { translatedStrings, errors } = this._getIdToTranslatedStringDictionary( language ); + return this._languages.map( language => { + const translatedStrings = this._getIdToTranslatedStringDictionary( language ); - // TODO: Windows paths. const outputPath = path.join( outputDirectory, `${ language }.js` ); - const stringifiedTranslations = JSON.stringify( translatedStrings, null, 2 ); - const outputBody = `CKEDITOR_TRANSLATIONS.add( '${ language }', ${ stringifiedTranslations } )`; + const stringifiedTranslations = JSON.stringify( translatedStrings ) + .replace( /"([a-z]+)":/g, '$1:' ); // removes unnecessary `""` around property names. + + const outputBody = `CKEDITOR_TRANSLATIONS.add('${ language }',${ stringifiedTranslations })`; - return { outputPath, outputBody, errors }; + return { outputPath, outputBody }; } ); } - // Walk through the `translationIdsDictionary` and find corresponding strings in target language's dictionary. + // Walk through the `translationIdsDictionary` and find corresponding strings in the target language's dictionary. // Use original strings if translated ones are missing. _getIdToTranslatedStringDictionary( lang ) { - const langDictionary = this.dictionary[ lang ]; + let langDictionary = this._dictionary[ lang ]; + + if ( !langDictionary ) { + this.emit( 'error', `No translation found for ${ lang } language.` ); + + // Fallback to the original translation strings. + langDictionary = {}; + } + const translatedStrings = {}; - const errors = []; - for ( const originalString in this.translationIdsDictionary ) { - const id = this.translationIdsDictionary[ originalString ]; + for ( const originalString in this._translationIdsDictionary ) { + const id = this._translationIdsDictionary[ originalString ]; const translatedString = langDictionary[ originalString ]; if ( !translatedString ) { - errors.push( `Missing translation for ${ originalString } for ${ lang } language.` ); + this.emit( 'error', `Missing translation for ${ originalString } for ${ lang } language.` ); } translatedStrings[ id ] = translatedString || originalString; } - return { - translatedStrings, - errors - }; + return translatedStrings; } - // Loads translations from the po file. + // Load translations from the PO files. _loadPoFile( language, pathToPoFile ) { if ( !fs.existsSync( pathToPoFile ) ) { return; @@ -127,11 +125,11 @@ module.exports = class MultipleLanguageTranslationService { const poFileContent = fs.readFileSync( pathToPoFile, 'utf-8' ); const parsedTranslationFile = createDictionaryFromPoFileContent( poFileContent ); - if ( !this.dictionary[ language ] ) { - this.dictionary[ language ] = {}; + if ( !this._dictionary[ language ] ) { + this._dictionary[ language ] = {}; } - const dictionary = this.dictionary[ language ]; + const dictionary = this._dictionary[ language ]; for ( const translationKey in parsedTranslationFile ) { // TODO: ensure that translation files can't use the same translationKey. @@ -139,15 +137,15 @@ module.exports = class MultipleLanguageTranslationService { } } - // Translates all t() call found in source text to the target language. + // Translate all t() call found in source text to the target language. _getId( originalString ) { // TODO - log when translation is missing. - let id = this.translationIdsDictionary[ originalString ]; + let id = this._translationIdsDictionary[ originalString ]; if ( !id ) { id = this._idCreator.getNextId(); - this.translationIdsDictionary[ originalString ] = id; + this._translationIdsDictionary[ originalString ] = id; } return id; diff --git a/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js index 59cd77a91..6f65876f1 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js @@ -9,32 +9,33 @@ const translateSource = require( './translateSource' ); const path = require( 'path' ); const fs = require( 'fs' ); const createDictionaryFromPoFileContent = require( './createdictionaryfrompofilecontent' ); +const { EventEmitter } = require( 'events' ); /** - * `SingleLanguageTranslationService` replaces `t()` call strings with translated one directly in the build + * `SingleLanguageTranslationService` replaces `t()` call strings with translated one directly in the build. */ -module.exports = class SingleLanguageTranslationService { - constructor( languages ) { - /** - * @readonly - */ - this.dictionary = {}; - - /** - * @readonly - */ - this.packagePaths = new Set(); - - this.language = languages[ 0 ]; +module.exports = class SingleLanguageTranslationService extends EventEmitter { + /** + * + * @param {String} language Target language. + */ + constructor( language ) { + super(); + + this._language = language; + + this._packagePaths = new Set(); + + this._dictionary = {}; } /** - * Translates file's source and replace `t()` call strings with translated strings. + * Translate file's source and replace `t()` call strings with translated strings. * * @param {String} source */ translateSource( source ) { - return translateSource( source, originalString => this.translateString( originalString ) ); + return translateSource( source, originalString => this._translateString( originalString ) ); } /** @@ -43,21 +44,25 @@ module.exports = class SingleLanguageTranslationService { * @param {String} pathToPackage Path to the package containing translations. */ loadPackage( pathToPackage ) { - if ( this.packagePaths.has( pathToPackage ) ) { + if ( this._packagePaths.has( pathToPackage ) ) { return; } - this.packagePaths.add( pathToPackage ); + this._packagePaths.add( pathToPackage ); - const pathToPoFile = this._getPathToPoFile( pathToPackage, this.language ); + const pathToPoFile = this._getPathToPoFile( pathToPackage, this._language ); this._loadPoFile( pathToPoFile ); } + /** + * That class doesn't generate any asset. + */ getAssets() { return []; } + // Load translations from the PO file. _loadPoFile( pathToPoFile ) { if ( !fs.existsSync( pathToPoFile ) ) { return; @@ -68,12 +73,18 @@ module.exports = class SingleLanguageTranslationService { for ( const translationKey in parsedTranslationFile ) { // TODO: ensure that translation files can't use the same translationKey. - this.dictionary[ translationKey ] = parsedTranslationFile[ translationKey ]; + this._dictionary[ translationKey ] = parsedTranslationFile[ translationKey ]; } } - translateString( originalString ) { - return this.dictionary[ originalString ]; + _translateString( originalString ) { + if ( !this._dictionary[ originalString ] ) { + this.emit( 'error', `Missing translation for ${ originalString } for ${ this._language } language.` ); + + return originalString; + } + + return this._dictionary[ originalString ]; } /** diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/index.js b/packages/ckeditor5-dev-webpack-plugin/lib/index.js index fa72ef88a..de7a0ac68 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/index.js @@ -5,18 +5,21 @@ 'use strict'; +const chalk = require( 'chalk' ); const serveTranslations = require( './serve-translations' ); -const { - MultipleLanguageTranslationService, - SingleLanguageTranslationService -} = require( '@ckeditor/ckeditor5-dev-utils' ).translations; +const SingleLanguageTranslationService = require( '@ckeditor/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice' ); +const MultipleLanguageTranslationService = require( '@ckeditor/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice' ); module.exports = class CKEditorWebpackPlugin { /** - * @param {Object} [options] - * @param {Array.} [options.packages] Array of directories in which packages will be looked for. - * @param {Object} [options.languages] - * TODO: Fix params. + * @param {Object} [options] Plugin options. + * @param {Array.} [options.languages] Target languages. + * @param {String} [options.outputDirectory='lang'] Output directory for the emitted translation files, + * should be relative to the webpack context. + * @param {Boolean} [options.optimizeBuildForOneLanguage] Optimized build for one language (directly replaces translation + * keys with the target language's strings. + * @param {Boolean} [options.throwErrorOnMissingTranslation] Throw when the translation is missing. + * By default original (english translation keys) are used when the target translation is missing. */ constructor( options = {} ) { this.options = options; @@ -31,7 +34,15 @@ module.exports = class CKEditorWebpackPlugin { if ( this.options.optimizeBuildForOneLanguage ) { if ( this.options.languages.length > 1 ) { - throw new Error( 'Only one language should be specified when `optimizeBuildForOneLanguage` option is on.' ); + throw new Error( chalk.red( + 'Only one language should be specified when `optimizeBuildForOneLanguage` option is on.' + ) ); + } + + if ( this.options.outputDirectory ) { + console.error( chalk.red( + '`outputDirectory` option does not work with `optimizeBuildForOneLanguage` option. It will be ignored.' + ) ); } translationService = new SingleLanguageTranslationService( this.options.languages[ 0 ] ); diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/serve-translations.js b/packages/ckeditor5-dev-webpack-plugin/lib/serve-translations.js index bccfcd730..e6bdc05f4 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/serve-translations.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/serve-translations.js @@ -1,35 +1,45 @@ const utils = require( './utils' ); const path = require( 'path' ); -const flatten = require( 'lodash/flatten' ); const chalk = require( 'chalk' ); /** * Serve translations depending on the used translation service and passed options. + * It takes care about whole Webpack compilation process. * - * @param {*} compiler Webpack compiler - * @param {Object} options Translation options - * @param {String} options.languages Target languages + * See https://webpack.js.org/api/compiler/#event-hooks and https://webpack.js.org/api/compilation/ + * for details about specific hooks. + * + * @param {Object} compiler Webpack compiler. + * @param {Object} options Translation options. + * @param {Array.} options.languages Target languages. + * @param {String} [options.outputDirectory='lang'] Output directory for the emitted translation files, + * should be relative to the webpack context. * @param {Boolean} [options.throwErrorOnMissingTranslation] Throw when the translation is missing. - * By default original (english strings) are used when the target translation is missing. - * @param {String} [options.outputDirectory='lang'] Output directory for the emitted translation files. + * By default original (english translation keys) are used when the target translation is missing. * @param {TranslationService} translationService */ module.exports = function serveTranslations( compiler, options, translationService ) { + const cwd = process.cwd(); + // Provides translateSource method for the `translatesourceloader` loader. compiler.options.translateSource = source => translationService.translateSource( source ); + // Watch for errors during translation process. + translationService.on( 'error', error => { + if ( options.throwErrorOnMissingTranslation ) { + throw new Error( chalk.red( error ) ); + } + + console.error( chalk.red( error ) ); + } ); + // Add ckeditor5-core translations before `translatesourceloader` starts translating. compiler.plugin( 'after-resolvers', () => { - compiler.resolvers.normal.resolve( - process.cwd(), - process.cwd(), - '@ckeditor/ckeditor5-core/src/editor/editor.js', - ( err, result ) => { - const pathToCoreTranslationPackage = result.match( utils.CKEditor5CoreRegExp )[ 0 ]; - - translationService.loadPackage( pathToCoreTranslationPackage ); - } - ); + compiler.resolvers.normal.resolve( cwd, cwd, '@ckeditor/ckeditor5-core/src/editor/editor.js', ( err, result ) => { + const pathToCoreTranslationPackage = result.match( utils.CKEditor5CoreRegExp )[ 0 ]; + + translationService.loadPackage( pathToCoreTranslationPackage ); + } ); } ); compiler.plugin( 'normal-module-factory', nmf => { @@ -52,11 +62,13 @@ module.exports = function serveTranslations( compiler, options, translationServi // Add package to the translations if the resource comes from ckeditor5-* package. function maybeLoadPackage( resolveOptions ) { const packageNameRegExp = utils.CKEditor5PackageNameRegExp; - const match = resolveOptions.resource.match( packageNameRegExp ); + const relativePathToResource = path.relative( cwd, resolveOptions.resource ); + + const match = relativePathToResource.match( packageNameRegExp ); if ( match ) { - const index = resolveOptions.resource.search( packageNameRegExp ) + match[ 0 ].length; - const pathToPackage = resolveOptions.resource.slice( 0, index ); + const index = relativePathToResource.search( packageNameRegExp ) + match[ 0 ].length; + const pathToPackage = relativePathToResource.slice( 0, index ); translationService.loadPackage( pathToPackage ); } @@ -64,24 +76,17 @@ module.exports = function serveTranslations( compiler, options, translationServi // Inject loader when the file comes from ckeditor5-* packages. function maybeAddLoader( resolveOptions ) { - if ( resolveOptions.resource.match( utils.CKEditor5PackageSrcFileRegExp ) ) { + const relativePathToResource = path.relative( cwd, resolveOptions.resource ); + + if ( relativePathToResource.match( utils.CKEditor5PackageSrcFileRegExp ) ) { resolveOptions.loaders.unshift( path.join( __dirname, 'translatesourceloader.js' ) ); } } + // At the end add assets generated from the PO files. function addAssetsToExistingOnes( destinationAssets ) { const generatedAssets = translationService.getAssets( { outputDirectory: options.outputDirectory } ); - const errors = flatten( generatedAssets.map( asset => asset.errors ) ); - - if ( errors.length && options.throwErrorOnMissingTranslation ) { - throw new Error( errors.join( '\n' ) + '\n' ); - } - - for ( const error of errors ) { - console.error( chalk.red( error ) ); - } - for ( const asset of generatedAssets ) { destinationAssets[ asset.outputPath ] = { source: () => asset.outputBody, @@ -90,3 +95,40 @@ module.exports = function serveTranslations( compiler, options, translationServi } } }; + +/** + * TranslationService interface. + * + * It should extend or mix NodeJS' EventEmitter to provide `on()` method. + * + * @interface TranslationService + */ + +/** + * Return assets + * + * @method #loadPackage + * @param {String} pathToPackage Path to the package. + */ + +/** + * Translate file's source to the target language. + * + * @method #translateSource + * @param {String} source File's source. + * @returns {String} + */ + +/** + * Get assets at the end of compilation. + * + * @method #getAssets + * @returns {Array.} + */ + +/** + * Error found during the translation process. + * + * @fires error + */ + From 94495e9429daac468c57ad50a3bc77ada80a60d6 Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Wed, 15 Nov 2017 14:34:47 +0100 Subject: [PATCH 05/33] Fixed file name and API docs. --- .../ckeditor5-dev-webpack-plugin/lib/index.js | 8 +-- .../lib/replacetcalls.js | 70 ------------------- ...e-translations.js => servetranslations.js} | 2 +- 3 files changed, 5 insertions(+), 75 deletions(-) delete mode 100644 packages/ckeditor5-dev-webpack-plugin/lib/replacetcalls.js rename packages/ckeditor5-dev-webpack-plugin/lib/{serve-translations.js => servetranslations.js} (96%) diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/index.js b/packages/ckeditor5-dev-webpack-plugin/lib/index.js index de7a0ac68..85ff3d13c 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/index.js @@ -6,7 +6,7 @@ 'use strict'; const chalk = require( 'chalk' ); -const serveTranslations = require( './serve-translations' ); +const serveTranslations = require( './servetranslations' ); const SingleLanguageTranslationService = require( '@ckeditor/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice' ); const MultipleLanguageTranslationService = require( '@ckeditor/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice' ); @@ -16,9 +16,9 @@ module.exports = class CKEditorWebpackPlugin { * @param {Array.} [options.languages] Target languages. * @param {String} [options.outputDirectory='lang'] Output directory for the emitted translation files, * should be relative to the webpack context. - * @param {Boolean} [options.optimizeBuildForOneLanguage] Optimized build for one language (directly replaces translation - * keys with the target language's strings. - * @param {Boolean} [options.throwErrorOnMissingTranslation] Throw when the translation is missing. + * @param {Boolean} [options.optimizeBuildForOneLanguage] Option that optimizes build for one language (directly replaces translation + * keys with the target language's strings. Webpack won't emit any language file with that option enabled. + * @param {Boolean} [options.throwErrorOnMissingTranslation] Option that make the plugin throw when the translation is missing. * By default original (english translation keys) are used when the target translation is missing. */ constructor( options = {} ) { diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/replacetcalls.js b/packages/ckeditor5-dev-webpack-plugin/lib/replacetcalls.js deleted file mode 100644 index 9e35b4266..000000000 --- a/packages/ckeditor5-dev-webpack-plugin/lib/replacetcalls.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const path = require( 'path' ); -const { TranslationService } = require( '@ckeditor/ckeditor5-dev-utils' ).translations; -const utils = require( './utils' ); - -/** - * Replaces all function call parameters with translated strings for the t function. - * - * @param {Object} compiler Webpack compiler. - * @param {String} language Language code, e.g en_US. - */ -module.exports = function replaceTCalls( compiler, language ) { - const translationService = new TranslationService( language ); - - compiler.options.translateSource = source => translationService.translateSource( source ); - - // Adds ckeditor5-core translations before translate-source-loader starts translating. - compiler.plugin( 'after-resolvers', () => { - compiler.resolvers.normal.resolve( - process.cwd(), - process.cwd(), - '@ckeditor/ckeditor5-core/src/editor/editor.js', - ( err, result ) => { - const pathToCoreTranslationPackage = result.match( utils.CKEditor5CoreRegExp )[ 0 ]; - - translationService.loadPackage( pathToCoreTranslationPackage ); - } - ); - } ); - - compiler.plugin( 'normal-module-factory', nmf => { - nmf.plugin( 'after-resolve', ( resolveOptions, done ) => { - maybeLoadPackage( resolveOptions ); - maybeAddLoader( resolveOptions ); - - done( null, resolveOptions ); - } ); - } ); - - // Adds package to the translations if the resource comes from ckeditor5-* package. - function maybeLoadPackage( resolveOptions ) { - const packageNameRegExp = utils.CKEditor5PackageNameRegExp; - - const relativePathToResource = path.relative( process.cwd(), resolveOptions.resource ); - - const match = relativePathToResource.match( packageNameRegExp ); - - if ( match ) { - const index = relativePathToResource.search( packageNameRegExp ) + match[ 0 ].length; - const pathToPackage = path.join( process.cwd(), relativePathToResource.slice( 0, index ) ); - - translationService.loadPackage( pathToPackage ); - } - } - - // Injects loader when the file comes from ckeditor5-* packages. - function maybeAddLoader( resolveOptions ) { - const relativePathToResource = path.relative( process.cwd(), resolveOptions.resource ); - - if ( relativePathToResource.match( utils.CKEditor5PackageSrcFileRegExp ) ) { - resolveOptions.loaders.unshift( path.join( __dirname, 'translatesourceloader.js' ) ); - } - } -}; diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/serve-translations.js b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js similarity index 96% rename from packages/ckeditor5-dev-webpack-plugin/lib/serve-translations.js rename to packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js index e6bdc05f4..1d895f23f 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/serve-translations.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js @@ -14,7 +14,7 @@ const chalk = require( 'chalk' ); * @param {Array.} options.languages Target languages. * @param {String} [options.outputDirectory='lang'] Output directory for the emitted translation files, * should be relative to the webpack context. - * @param {Boolean} [options.throwErrorOnMissingTranslation] Throw when the translation is missing. + * @param {Boolean} [options.throwErrorOnMissingTranslation] Option that make this function throw when the translation is missing. * By default original (english translation keys) are used when the target translation is missing. * @param {TranslationService} translationService */ From e7a11f712645ae23582314829cfda2cc6acca3c8 Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Wed, 15 Nov 2017 16:00:40 +0100 Subject: [PATCH 06/33] Added tests for `SingleLanguageTranslationService` and `translateSource` files. --- .../multiplelanguagetranslationservice.js | 11 ++- .../singlelanguagetranslationservice.js | 10 ++- .../lib/translations/translatesource.js | 8 +- ...js => singlelanguagetranslationservice.js} | 79 +++++++------------ .../tests/translations/translatesource.js | 55 +++++++++++++ 5 files changed, 105 insertions(+), 58 deletions(-) rename packages/ckeditor5-dev-utils/tests/translations/{translationservice.js => singlelanguagetranslationservice.js} (54%) create mode 100644 packages/ckeditor5-dev-utils/tests/translations/translatesource.js diff --git a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js index a433dfa24..4260af38c 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js @@ -42,10 +42,18 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { /** * Translate file's source and replace `t()` call strings with short ids. * + * @fires error * @param {String} source + * @returns {String} */ translateSource( source ) { - return translateSource( source, translationKey => this._getId( translationKey ) ); + const { output, errors } = translateSource( source, originalString => this._translateString( originalString ) ); + + for ( const error of errors ) { + this.emit( 'error', error ); + } + + return output; } /** @@ -70,6 +78,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { /** * Return an array of assets based on the stored dictionaries. * + * @fires error * @param {Object} param0 * @param {String} [param0.outputDirectory] * @returns {Array.} diff --git a/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js index 6f65876f1..d3ce59d77 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js @@ -32,10 +32,18 @@ module.exports = class SingleLanguageTranslationService extends EventEmitter { /** * Translate file's source and replace `t()` call strings with translated strings. * + * @fires error * @param {String} source + * @returns {String} */ translateSource( source ) { - return translateSource( source, originalString => this._translateString( originalString ) ); + const { output, errors } = translateSource( source, originalString => this._translateString( originalString ) ); + + for ( const error of errors ) { + this.emit( 'error', error ); + } + + return output; } /** diff --git a/packages/ckeditor5-dev-utils/lib/translations/translatesource.js b/packages/ckeditor5-dev-utils/lib/translations/translatesource.js index 1bc30e8ea..b85031456 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/translatesource.js +++ b/packages/ckeditor5-dev-utils/lib/translations/translatesource.js @@ -8,7 +8,6 @@ const acorn = require( 'acorn' ); const walk = require( 'acorn/dist/walk' ); const escodegen = require( 'escodegen' ); -const logger = require( '../logger' )(); /** * Parses source, translates `t()` call arguments and returns modified output. @@ -20,6 +19,7 @@ const logger = require( '../logger' )(); module.exports = function translateSource( source, translateString ) { const comments = []; const tokens = []; + const errors = []; const ast = acorn.parse( source, { sourceType: 'module', @@ -37,7 +37,7 @@ module.exports = function translateSource( source, translateString ) { } if ( node.arguments[ 0 ].type !== 'Literal' ) { - logger.error( 'First t() call argument should be a string literal.' ); + errors.push( 'First t() call argument should be a string literal.' ); return; } @@ -49,7 +49,7 @@ module.exports = function translateSource( source, translateString ) { // Optimization for files without t() calls. if ( !changesInCode ) { - return source; + return { output: source, errors }; } escodegen.attachComments( ast, comments, tokens ); @@ -57,5 +57,5 @@ module.exports = function translateSource( source, translateString ) { comment: true } ); - return output; + return { output, errors }; }; diff --git a/packages/ckeditor5-dev-utils/tests/translations/translationservice.js b/packages/ckeditor5-dev-utils/tests/translations/singlelanguagetranslationservice.js similarity index 54% rename from packages/ckeditor5-dev-utils/tests/translations/translationservice.js rename to packages/ckeditor5-dev-utils/tests/translations/singlelanguagetranslationservice.js index 84da00a4f..d0d301bdb 100644 --- a/packages/ckeditor5-dev-utils/tests/translations/translationservice.js +++ b/packages/ckeditor5-dev-utils/tests/translations/singlelanguagetranslationservice.js @@ -11,26 +11,20 @@ const path = require( 'path' ); const proxyquire = require( 'proxyquire' ); describe( 'translations', () => { - describe( 'TranslationService', () => { - let TranslationService, stubs, files, fileContents, sandbox; + describe( 'SingleLanguageTranslationService', () => { + let SingleLanguageTranslationService, stubs, files, fileContents, sandbox; beforeEach( () => { sandbox = sinon.sandbox.create(); stubs = { - logger: { - info: sandbox.stub(), - warning: sandbox.stub(), - error: sandbox.stub() - }, fs: { existsSync: path => files.includes( path ), readFileSync: path => fileContents[ path ] } }; - TranslationService = proxyquire( '../../lib/translations/translationservice', { - '../logger': () => stubs.logger, + SingleLanguageTranslationService = proxyquire( '../../lib/translations/singlelanguagetranslationservice', { 'fs': stubs.fs } ); } ); @@ -40,37 +34,16 @@ describe( 'translations', () => { } ); describe( 'constructor()', () => { - it( 'should be able to use custom function that returns path to the po file', () => { - const pathToTranslations = path.join( 'customPathToPackage', 'lang', 'translations', 'pl.po' ); + it( 'should initialize `SingleLanguageTranslationService`', () => { + const translationService = new SingleLanguageTranslationService( 'pl' ); - files = [ pathToTranslations ]; - - fileContents = { - [ pathToTranslations ]: [ - 'msgctxt "Label for the Save button."', - 'msgid "Save"', - 'msgstr "Zapisz"', - '' - ].join( '\n' ) - }; - - const translationService = new TranslationService( 'pl', { - getPathToPoFile: ( pathToPackage, languageCode ) => { - return path.join( pathToPackage, 'lang', 'translations', `${ languageCode }.po` ); - } - } ); - - translationService.loadPackage( 'customPathToPackage' ); - - expect( Array.from( translationService.dictionary ) ).to.deep.equal( [ - [ 'Save', 'Zapisz' ] - ] ); + expect( translationService ).to.be.instanceof( SingleLanguageTranslationService ); } ); } ); describe( 'loadPackage()', () => { it( 'should load po file from the package and load translations', () => { - const translationService = new TranslationService( 'pl' ); + const translationService = new SingleLanguageTranslationService( 'pl' ); const pathToTranslations = path.join( 'pathToPackage', 'lang', 'translations', 'pl.po' ); files = [ pathToTranslations ]; @@ -86,24 +59,22 @@ describe( 'translations', () => { translationService.loadPackage( 'pathToPackage' ); - expect( Array.from( translationService.dictionary ) ).to.deep.equal( [ - [ 'Save', 'Zapisz' ] - ] ); + expect( translationService._dictionary ).to.deep.equal( { 'Save': 'Zapisz' } ); } ); it( 'should do nothing if the po file does not exist', () => { - const translationService = new TranslationService( 'pl' ); + const translationService = new SingleLanguageTranslationService( 'pl' ); files = []; fileContents = {}; translationService.loadPackage( 'pathToPackage' ); - expect( Array.from( translationService.dictionary ) ).to.deep.equal( [] ); + expect( translationService._dictionary ).to.deep.equal( {} ); } ); it( 'should load po file from the package only once', () => { - const translationService = new TranslationService( 'pl' ); + const translationService = new SingleLanguageTranslationService( 'pl' ); const loadPoFileSpy = sandbox.stub( translationService, '_loadPoFile' ); translationService.loadPackage( 'pathToPackage' ); @@ -115,10 +86,10 @@ describe( 'translations', () => { describe( 'translateSource()', () => { it( 'should translate t() calls in the code', () => { - const translationService = new TranslationService( 'pl' ); + const translationService = new SingleLanguageTranslationService( 'pl' ); const source = 't( \'Cancel\' )'; - translationService.dictionary.set( 'Cancel', 'Anuluj' ); + translationService._dictionary.Cancel = 'Anuluj'; const result = translationService.translateSource( source ); @@ -126,7 +97,7 @@ describe( 'translations', () => { } ); it( 'should return original source if there is no t() calls in the code', () => { - const translationService = new TranslationService( 'pl' ); + const translationService = new SingleLanguageTranslationService( 'pl' ); const source = 'translate( \'Cancel\' )'; const result = translationService.translateSource( source ); @@ -134,28 +105,32 @@ describe( 'translations', () => { expect( result ).to.equal( 'translate( \'Cancel\' )' ); } ); - it( 'should lg the error and keep original string if the translation misses', () => { - const translationService = new TranslationService( 'pl' ); + it( 'should emit an error and keep original string if the translation is missing', () => { + const translationService = new SingleLanguageTranslationService( 'pl' ); const source = 't( \'Cancel\' )'; + const spy = sandbox.spy(); + translationService.on( 'error', spy ); + const result = translationService.translateSource( source ); expect( result ).to.equal( 't(\'Cancel\');' ); - sinon.assert.calledOnce( stubs.logger.error ); - sinon.assert.calledWithExactly( stubs.logger.error, 'Missing translation for: Cancel.' ); + sinon.assert.calledOnce( spy ); + sinon.assert.calledWithExactly( spy, 'Missing translation for Cancel for pl language.' ); } ); it( 'should throw an error when the t is called with the variable', () => { - const translationService = new TranslationService( 'pl' ); + const translationService = new SingleLanguageTranslationService( 'pl' ); const source = 'const cancel = \'Cancel\';t( cancel );'; + const spy = sandbox.spy(); + translationService.on( 'error', spy ); + const result = translationService.translateSource( source ); expect( result ).to.equal( 'const cancel = \'Cancel\';t( cancel );' ); - sinon.assert.calledOnce( stubs.logger.error ); - sinon.assert.calledWithExactly( - stubs.logger.error, 'First t() call argument should be a string literal.' - ); + sinon.assert.calledOnce( spy ); + sinon.assert.calledWithExactly( spy, 'First t() call argument should be a string literal.' ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-utils/tests/translations/translatesource.js b/packages/ckeditor5-dev-utils/tests/translations/translatesource.js new file mode 100644 index 000000000..3fc28f9d9 --- /dev/null +++ b/packages/ckeditor5-dev-utils/tests/translations/translatesource.js @@ -0,0 +1,55 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +const { expect } = require( 'chai' ); +const sinon = require( 'sinon' ); +const translateSource = require( '../../lib/translations/translatesource' ); + +describe( 'translations', () => { + describe( 'translateSource', () => { + let sandbox, translations; + const translateString = translationKey => translations[ translationKey ]; + + beforeEach( () => { + sandbox = sinon.sandbox.create(); + + translations = { 'Cancel': 'Anuluj' }; + } ); + + afterEach( () => { + sandbox.restore(); + } ); + + it( 'should translate t() calls in the code', () => { + const source = 't( \'Cancel\' )'; + + const { output, errors } = translateSource( source, translateString ); + + expect( output ).to.equal( 't(\'Anuluj\');' ); + expect( errors.length ).to.equal( 0 ); + } ); + + it( 'should return original source if there is no t() calls in the code', () => { + const source = 'translate( \'Cancel\' )'; + + const { output, errors } = translateSource( source, translateString ); + + expect( output ).to.equal( 'translate( \'Cancel\' )' ); + expect( errors.length ).to.equal( 0 ); + } ); + + it( 'should throw an error when the t is called with the variable', () => { + const source = 'const cancel = \'Cancel\';t( cancel );'; + + const { output, errors } = translateSource( source, translateString ); + + expect( output ).to.equal( 'const cancel = \'Cancel\';t( cancel );' ); + // TODO: improve error message. + expect( errors ).to.deep.equal( [ 'First t() call argument should be a string literal.' ] ); + } ); + } ); +} ); From 70acef0340ff4aa2af66291f203c04731610ef55 Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Wed, 15 Nov 2017 16:57:39 +0100 Subject: [PATCH 07/33] Added tests for MultipleLanguageTranslationService. --- .../multiplelanguagetranslationservice.js | 7 +- .../multiplelanguagetranslationservice.js | 324 ++++++++++++++++++ .../singlelanguagetranslationservice.js | 9 + .../tests/translations/translatesource.js | 2 +- 4 files changed, 336 insertions(+), 6 deletions(-) create mode 100644 packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js diff --git a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js index 4260af38c..c1310005c 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js @@ -47,7 +47,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * @returns {String} */ translateSource( source ) { - const { output, errors } = translateSource( source, originalString => this._translateString( originalString ) ); + const { output, errors } = translateSource( source, originalString => this._getId( originalString ) ); for ( const error of errors ) { this.emit( 'error', error ); @@ -83,7 +83,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * @param {String} [param0.outputDirectory] * @returns {Array.} */ - getAssets( { outputDirectory = 'lang' } ) { + getAssets( { outputDirectory = 'lang' } = {} ) { return this._languages.map( language => { const translatedStrings = this._getIdToTranslatedStringDictionary( language ); @@ -141,15 +141,12 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { const dictionary = this._dictionary[ language ]; for ( const translationKey in parsedTranslationFile ) { - // TODO: ensure that translation files can't use the same translationKey. dictionary[ translationKey ] = parsedTranslationFile[ translationKey ]; } } // Translate all t() call found in source text to the target language. _getId( originalString ) { - // TODO - log when translation is missing. - let id = this._translationIdsDictionary[ originalString ]; if ( !id ) { diff --git a/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js new file mode 100644 index 000000000..1d6dd9bff --- /dev/null +++ b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js @@ -0,0 +1,324 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +const { expect } = require( 'chai' ); +const sinon = require( 'sinon' ); +const path = require( 'path' ); +const proxyquire = require( 'proxyquire' ); + +describe( 'translations', () => { + describe( 'MultipleLanguageTranslationService', () => { + let MultipleLanguageTranslationService, stubs, files, fileContents, sandbox; + + beforeEach( () => { + sandbox = sinon.sandbox.create(); + + stubs = { + fs: { + existsSync: path => files.includes( path ), + readFileSync: path => fileContents[ path ] + } + }; + + MultipleLanguageTranslationService = proxyquire( '../../lib/translations/multiplelanguagetranslationservice', { + 'fs': stubs.fs + } ); + } ); + + afterEach( () => { + sandbox.restore(); + } ); + + describe( 'constructor()', () => { + it( 'should initialize `SingleLanguageTranslationService`', () => { + const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); + + expect( translationService ).to.be.instanceof( MultipleLanguageTranslationService ); + } ); + } ); + + describe( 'loadPackage()', () => { + it( 'should load po file from the package and load translations', () => { + const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); + const pathToPlTranslations = path.join( 'pathToPackage', 'lang', 'translations', 'pl.po' ); + const pathToDeTranslations = path.join( 'pathToPackage', 'lang', 'translations', 'de.po' ); + + files = [ pathToPlTranslations, pathToDeTranslations ]; + + fileContents = { + [ pathToPlTranslations ]: [ + 'msgctxt "Label for the Save button."', + 'msgid "Save"', + 'msgstr "Zapisz"', + '' + ].join( '\n' ), + [ pathToDeTranslations ]: [ + 'msgctxt "Label for the Save button."', + 'msgid "Save"', + 'msgstr "Speichern"', + '' + ].join( '\n' ) + }; + + translationService.loadPackage( 'pathToPackage' ); + + expect( translationService._dictionary ).to.deep.equal( { + pl: { + 'Save': 'Zapisz' + }, + de: { + 'Save': 'Speichern' + } + } ); + } ); + + it( 'should do nothing if the po file does not exist', () => { + const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); + + files = []; + fileContents = {}; + + translationService.loadPackage( 'pathToPackage' ); + + expect( translationService._dictionary ).to.deep.equal( {} ); + } ); + + it( 'should load po file from the package only once per language', () => { + const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); + const loadPoFileSpy = sandbox.stub( translationService, '_loadPoFile' ); + + translationService.loadPackage( 'pathToPackage' ); + translationService.loadPackage( 'pathToPackage' ); + translationService.loadPackage( 'pathToPackage' ); + + sinon.assert.calledTwice( loadPoFileSpy ); + } ); + } ); + + describe( 'translateSource()', () => { + it( 'should replace t() call params with the translation id, starting with `a`', () => { + const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); + const source = 't( \'Cancel\' ), t( \'Save\' );'; + + const result = translationService.translateSource( source ); + + expect( result ).to.equal( 't(\'a\'), t(\'b\');' ); + expect( translationService._translationIdsDictionary ).to.deep.equal( { + Cancel: 'a', + Save: 'b' + } ); + } ); + + it( 'should return original source if there is no t() calls in the code', () => { + const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); + const source = 'translate( \'Cancel\' )'; + + const result = translationService.translateSource( source ); + + expect( result ).to.equal( 'translate( \'Cancel\' )' ); + + expect( translationService._translationIdsDictionary ).to.deep.equal( {} ); + } ); + } ); + + describe( 'getAssets()', () => { + it( 'should return an array of assets', () => { + const translationService = new MultipleLanguageTranslationService( [ 'pl', 'en' ] ); + + translationService._translationIdsDictionary = { + Cancel: 'a', + Save: 'b' + }; + + translationService._dictionary = { + pl: { + Cancel: 'Anuluj', + Save: 'Zapisz', + }, + en: { + Cancel: 'Cancel', + Save: 'Save' + } + }; + + const assets = translationService.getAssets(); + + expect( assets ).to.deep.equal( [ + { + outputPath: path.join( 'lang', 'pl.js' ), + outputBody: 'CKEDITOR_TRANSLATIONS.add(\'pl\',{a:"Anuluj",b:"Zapisz"})' + }, + { + outputPath: path.join( 'lang', 'en.js' ), + outputBody: 'CKEDITOR_TRANSLATIONS.add(\'en\',{a:"Cancel",b:"Save"})' + } + ] ); + } ); + + it( 'should emit an error if the language is not present in language list', () => { + const translationService = new MultipleLanguageTranslationService( [ 'pl', 'xxx' ] ); + const spy = sandbox.spy(); + + translationService.on( 'error', spy ); + + translationService._translationIdsDictionary = { + Cancel: 'a', + Save: 'b' + }; + + translationService._dictionary = { + pl: { + Cancel: 'Anuluj', + Save: 'Zapisz', + } + }; + + translationService.getAssets(); + + sinon.assert.calledThrice( spy ); + sinon.assert.calledWithExactly( spy, 'No translation found for xxx language.' ); + sinon.assert.calledWithExactly( spy, 'Missing translation for Cancel for xxx language.' ); + sinon.assert.calledWithExactly( spy, 'Missing translation for Save for xxx language.' ); + } ); + + it( 'should feed missing translation with the translation key if the translated string is missing', () => { + const translationService = new MultipleLanguageTranslationService( [ 'pl', 'xxx' ] ); + const spy = sandbox.spy(); + + translationService.on( 'error', spy ); + + translationService._translationIdsDictionary = { + Cancel: 'a', + Save: 'b' + }; + + translationService._dictionary = { + pl: { + Cancel: 'Anuluj', + Save: 'Zapisz', + } + }; + + const assets = translationService.getAssets(); + + expect( assets ).to.deep.equal( [ + { + outputPath: path.join( 'lang', 'pl.js' ), + outputBody: 'CKEDITOR_TRANSLATIONS.add(\'pl\',{a:"Anuluj",b:"Zapisz"})' + }, + { + outputPath: path.join( 'lang', 'xxx.js' ), + outputBody: 'CKEDITOR_TRANSLATIONS.add(\'xxx\',{a:"Cancel",b:"Save"})' + } + ] ); + } ); + + it( 'should bound to assets only used translations', () => { + const translationService = new MultipleLanguageTranslationService( [ 'pl' ] ); + const spy = sandbox.spy(); + + translationService.on( 'error', spy ); + + translationService._translationIdsDictionary = { + Cancel: 'a', + Save: 'b' + }; + + translationService._dictionary = { + pl: { + Cancel: 'Anuluj', + Save: 'Zapisz', + Close: 'Zamknij', + } + }; + + const assets = translationService.getAssets(); + + // Note that the last translation from the above dictionary is skipped. + expect( assets ).to.deep.equal( [ + { + outputPath: path.join( 'lang', 'pl.js' ), + outputBody: 'CKEDITOR_TRANSLATIONS.add(\'pl\',{a:"Anuluj",b:"Zapisz"})' + } + ] ); + } ); + } ); + + describe( '_getPathToPackage', () => { + it( 'should be overridable to enable providing custom path to translation files', () => { + class CustomTranslationService extends MultipleLanguageTranslationService { + _getPathToPoFile( pathToPackage, languageCode ) { + return path.join( 'custom', 'path', 'to', pathToPackage, languageCode + '.PO' ); + } + } + + const translationService = new CustomTranslationService( [ 'en' ] ); + + const pathToTranslations = path.join( 'custom', 'path', 'to', 'pathToPackage', 'en.PO' ); + + files = [ pathToTranslations ]; + + fileContents = { + [ pathToTranslations ]: [ + 'msgctxt "Label for the Save button."', + 'msgid "Save"', + 'msgstr "Save"', + '' + ].join( '\n' ) + }; + + translationService.loadPackage( 'pathToPackage' ); + + expect( translationService._dictionary ).to.deep.equal( { + en: { + 'Save': 'Save' + } + } ); + } ); + } ); + + describe( 'integration test', () => { + it( 'test #1', () => { + const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); + const pathToPlTranslations = path.join( 'pathToPackage', 'lang', 'translations', 'pl.po' ); + const pathToDeTranslations = path.join( 'pathToPackage', 'lang', 'translations', 'de.po' ); + + files = [ pathToPlTranslations, pathToDeTranslations ]; + + fileContents = { + [ pathToPlTranslations ]: [ + 'msgctxt "Label for the Save button."', + 'msgid "Save"', + 'msgstr "Zapisz"', + '' + ].join( '\n' ), + [ pathToDeTranslations ]: [ + 'msgctxt "Label for the Save button."', + 'msgid "Save"', + 'msgstr "Speichern"', + '' + ].join( '\n' ) + }; + + translationService.loadPackage( 'pathToPackage' ); + translationService.translateSource( 't( \'Save\' );' ); + const assets = translationService.getAssets(); + + expect( assets ).to.deep.equal( [ + { + outputPath: path.join( 'lang', 'pl.js' ), + outputBody: 'CKEDITOR_TRANSLATIONS.add(\'pl\',{a:"Zapisz"})' + }, + { + outputPath: path.join( 'lang', 'de.js' ), + outputBody: 'CKEDITOR_TRANSLATIONS.add(\'de\',{a:"Speichern"})' + } + ] ); + } ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-dev-utils/tests/translations/singlelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/tests/translations/singlelanguagetranslationservice.js index d0d301bdb..9e61b6d23 100644 --- a/packages/ckeditor5-dev-utils/tests/translations/singlelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/tests/translations/singlelanguagetranslationservice.js @@ -133,5 +133,14 @@ describe( 'translations', () => { sinon.assert.calledWithExactly( spy, 'First t() call argument should be a string literal.' ); } ); } ); + + describe( 'getAssets()', () => { + it( 'should return an empty array', () => { + const translationService = new SingleLanguageTranslationService( [ 'pl', 'de' ] ); + const assets = translationService.getAssets(); + + expect( assets ).to.deep.equal( [] ); + } ); + } ); } ); } ); diff --git a/packages/ckeditor5-dev-utils/tests/translations/translatesource.js b/packages/ckeditor5-dev-utils/tests/translations/translatesource.js index 3fc28f9d9..098c04391 100644 --- a/packages/ckeditor5-dev-utils/tests/translations/translatesource.js +++ b/packages/ckeditor5-dev-utils/tests/translations/translatesource.js @@ -10,7 +10,7 @@ const sinon = require( 'sinon' ); const translateSource = require( '../../lib/translations/translatesource' ); describe( 'translations', () => { - describe( 'translateSource', () => { + describe( 'translateSource()', () => { let sandbox, translations; const translateString = translationKey => translations[ translationKey ]; From e2f0bcfebbb7912ea573a24adb3f0a0f3b8c63a8 Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Wed, 15 Nov 2017 17:43:54 +0100 Subject: [PATCH 08/33] Provided better error messages. --- .../multiplelanguagetranslationservice.js | 8 +++++--- .../translations/singlelanguagetranslationservice.js | 12 +++++++----- .../lib/translations/translatesource.js | 5 +++-- .../multiplelanguagetranslationservice.js | 4 ++-- .../translations/singlelanguagetranslationservice.js | 12 ++++++------ .../tests/translations/translatesource.js | 9 ++++----- .../lib/servetranslations.js | 2 +- .../lib/translatesourceloader.js | 2 +- 8 files changed, 29 insertions(+), 25 deletions(-) diff --git a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js index c1310005c..a2aac803f 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js @@ -43,11 +43,13 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * Translate file's source and replace `t()` call strings with short ids. * * @fires error - * @param {String} source + * @param {String} source Source of the file. + * @param {String} fileName File name. * @returns {String} */ - translateSource( source ) { - const { output, errors } = translateSource( source, originalString => this._getId( originalString ) ); + translateSource( source, fileName ) { + const translate = originalString => this._getId( originalString ); + const { output, errors } = translateSource( source, fileName, translate ); for ( const error of errors ) { this.emit( 'error', error ); diff --git a/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js index d3ce59d77..47ab5ed22 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js @@ -33,11 +33,13 @@ module.exports = class SingleLanguageTranslationService extends EventEmitter { * Translate file's source and replace `t()` call strings with translated strings. * * @fires error - * @param {String} source + * @param {String} source Source of the file. + * @param {String} fileName File name. * @returns {String} */ - translateSource( source ) { - const { output, errors } = translateSource( source, originalString => this._translateString( originalString ) ); + translateSource( source, fileName ) { + const translate = originalString => this._translateString( originalString, fileName ); + const { output, errors } = translateSource( source, fileName, translate ); for ( const error of errors ) { this.emit( 'error', error ); @@ -85,9 +87,9 @@ module.exports = class SingleLanguageTranslationService extends EventEmitter { } } - _translateString( originalString ) { + _translateString( originalString, sourceFile ) { if ( !this._dictionary[ originalString ] ) { - this.emit( 'error', `Missing translation for ${ originalString } for ${ this._language } language.` ); + this.emit( 'error', `Missing translation for ${ originalString } for ${ this._language } language in ${ sourceFile }.` ); return originalString; } diff --git a/packages/ckeditor5-dev-utils/lib/translations/translatesource.js b/packages/ckeditor5-dev-utils/lib/translations/translatesource.js index b85031456..f23965adc 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/translatesource.js +++ b/packages/ckeditor5-dev-utils/lib/translations/translatesource.js @@ -13,10 +13,11 @@ const escodegen = require( 'escodegen' ); * Parses source, translates `t()` call arguments and returns modified output. * * @param {String} source JS source text which will be translated. + * @param {String} sourceFile JS source file name which will be translated. * @param {Function} translateString Function that will translate matched string to the destination language or hash. * @returns {String} Transformed source. */ -module.exports = function translateSource( source, translateString ) { +module.exports = function translateSource( source, sourceFile, translateString ) { const comments = []; const tokens = []; const errors = []; @@ -37,7 +38,7 @@ module.exports = function translateSource( source, translateString ) { } if ( node.arguments[ 0 ].type !== 'Literal' ) { - errors.push( 'First t() call argument should be a string literal.' ); + errors.push( `First t() call argument should be a string literal in ${ sourceFile }.` ); return; } diff --git a/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js index 1d6dd9bff..c936a9b23 100644 --- a/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js @@ -104,7 +104,7 @@ describe( 'translations', () => { const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); const source = 't( \'Cancel\' ), t( \'Save\' );'; - const result = translationService.translateSource( source ); + const result = translationService.translateSource( source, 'file.js' ); expect( result ).to.equal( 't(\'a\'), t(\'b\');' ); expect( translationService._translationIdsDictionary ).to.deep.equal( { @@ -117,7 +117,7 @@ describe( 'translations', () => { const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); const source = 'translate( \'Cancel\' )'; - const result = translationService.translateSource( source ); + const result = translationService.translateSource( source, 'file.js' ); expect( result ).to.equal( 'translate( \'Cancel\' )' ); diff --git a/packages/ckeditor5-dev-utils/tests/translations/singlelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/tests/translations/singlelanguagetranslationservice.js index 9e61b6d23..a22a9b707 100644 --- a/packages/ckeditor5-dev-utils/tests/translations/singlelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/tests/translations/singlelanguagetranslationservice.js @@ -91,7 +91,7 @@ describe( 'translations', () => { translationService._dictionary.Cancel = 'Anuluj'; - const result = translationService.translateSource( source ); + const result = translationService.translateSource( source, 'file.js' ); expect( result ).to.equal( 't(\'Anuluj\');' ); } ); @@ -100,7 +100,7 @@ describe( 'translations', () => { const translationService = new SingleLanguageTranslationService( 'pl' ); const source = 'translate( \'Cancel\' )'; - const result = translationService.translateSource( source ); + const result = translationService.translateSource( source, 'file.js' ); expect( result ).to.equal( 'translate( \'Cancel\' )' ); } ); @@ -112,11 +112,11 @@ describe( 'translations', () => { const spy = sandbox.spy(); translationService.on( 'error', spy ); - const result = translationService.translateSource( source ); + const result = translationService.translateSource( source, 'file.js' ); expect( result ).to.equal( 't(\'Cancel\');' ); sinon.assert.calledOnce( spy ); - sinon.assert.calledWithExactly( spy, 'Missing translation for Cancel for pl language.' ); + sinon.assert.calledWithExactly( spy, 'Missing translation for Cancel for pl language in file.js.' ); } ); it( 'should throw an error when the t is called with the variable', () => { @@ -126,11 +126,11 @@ describe( 'translations', () => { const spy = sandbox.spy(); translationService.on( 'error', spy ); - const result = translationService.translateSource( source ); + const result = translationService.translateSource( source, 'file.js' ); expect( result ).to.equal( 'const cancel = \'Cancel\';t( cancel );' ); sinon.assert.calledOnce( spy ); - sinon.assert.calledWithExactly( spy, 'First t() call argument should be a string literal.' ); + sinon.assert.calledWithExactly( spy, 'First t() call argument should be a string literal in file.js.' ); } ); } ); diff --git a/packages/ckeditor5-dev-utils/tests/translations/translatesource.js b/packages/ckeditor5-dev-utils/tests/translations/translatesource.js index 098c04391..903e59d02 100644 --- a/packages/ckeditor5-dev-utils/tests/translations/translatesource.js +++ b/packages/ckeditor5-dev-utils/tests/translations/translatesource.js @@ -27,7 +27,7 @@ describe( 'translations', () => { it( 'should translate t() calls in the code', () => { const source = 't( \'Cancel\' )'; - const { output, errors } = translateSource( source, translateString ); + const { output, errors } = translateSource( source, 'file.js', translateString ); expect( output ).to.equal( 't(\'Anuluj\');' ); expect( errors.length ).to.equal( 0 ); @@ -36,7 +36,7 @@ describe( 'translations', () => { it( 'should return original source if there is no t() calls in the code', () => { const source = 'translate( \'Cancel\' )'; - const { output, errors } = translateSource( source, translateString ); + const { output, errors } = translateSource( source, 'file.js', translateString ); expect( output ).to.equal( 'translate( \'Cancel\' )' ); expect( errors.length ).to.equal( 0 ); @@ -45,11 +45,10 @@ describe( 'translations', () => { it( 'should throw an error when the t is called with the variable', () => { const source = 'const cancel = \'Cancel\';t( cancel );'; - const { output, errors } = translateSource( source, translateString ); + const { output, errors } = translateSource( source, 'file.js', translateString ); expect( output ).to.equal( 'const cancel = \'Cancel\';t( cancel );' ); - // TODO: improve error message. - expect( errors ).to.deep.equal( [ 'First t() call argument should be a string literal.' ] ); + expect( errors ).to.deep.equal( [ 'First t() call argument should be a string literal in file.js.' ] ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js index 1d895f23f..4d4292f6d 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js @@ -22,7 +22,7 @@ module.exports = function serveTranslations( compiler, options, translationServi const cwd = process.cwd(); // Provides translateSource method for the `translatesourceloader` loader. - compiler.options.translateSource = source => translationService.translateSource( source ); + compiler.options.translateSource = ( source, sourceFile ) => translationService.translateSource( source, sourceFile ); // Watch for errors during translation process. translationService.on( 'error', error => { diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/translatesourceloader.js b/packages/ckeditor5-dev-webpack-plugin/lib/translatesourceloader.js index b64fee81a..c1b710e65 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/translatesourceloader.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/translatesourceloader.js @@ -13,5 +13,5 @@ * @returns {String} */ module.exports = function translateSourceLoader( source ) { - return this.options.translateSource( source ); + return this.options.translateSource( source, this.resourcePath ); }; From d3fcc643723f8cf5152fc4d35f06588756ce11bb Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Wed, 15 Nov 2017 17:48:06 +0100 Subject: [PATCH 09/33] Fixed imports. --- .../lib/translations/multiplelanguagetranslationservice.js | 2 +- .../lib/translations/singlelanguagetranslationservice.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js index a2aac803f..148f42cc9 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js @@ -8,7 +8,7 @@ const path = require( 'path' ); const fs = require( 'fs' ); const createDictionaryFromPoFileContent = require( './createdictionaryfrompofilecontent' ); -const translateSource = require( './translateSource' ); +const translateSource = require( './translatesource' ); const ShortIdGenerator = require( './shortidgenerator' ); const { EventEmitter } = require( 'events' ); diff --git a/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js index 47ab5ed22..9b2a3ea69 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js @@ -5,7 +5,7 @@ 'use strict'; -const translateSource = require( './translateSource' ); +const translateSource = require( './translatesource' ); const path = require( 'path' ); const fs = require( 'fs' ); const createDictionaryFromPoFileContent = require( './createdictionaryfrompofilecontent' ); From 2928db3cdf015573c7b3340ba95170ea86ccb44f Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Wed, 15 Nov 2017 18:13:45 +0100 Subject: [PATCH 10/33] Added test for `translateSourceLoader`. --- .../multiplelanguagetranslationservice.js | 6 ++-- .../singlelanguagetranslationservice.js | 1 - .../tests/translatesourceloader.js | 36 +++++++++++++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 packages/ckeditor5-dev-webpack-plugin/tests/translatesourceloader.js diff --git a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js index 148f42cc9..a65378a98 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js @@ -16,7 +16,7 @@ const { EventEmitter } = require( 'events' ); * `MultipleLanguageTranslationService` replaces `t()` call params with short ids * and provides assets that translate those ids to target languages. * - * translationKey - original english string that occur in `t()` call params. + * `translationKey` - original english string that occur in `t()` call params. */ module.exports = class MultipleLanguageTranslationService extends EventEmitter { /** @@ -36,7 +36,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { // @type {Object.} this._translationIdsDictionary = {}; - this._idCreator = new ShortIdGenerator(); + this._idGenerator = new ShortIdGenerator(); } /** @@ -152,7 +152,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { let id = this._translationIdsDictionary[ originalString ]; if ( !id ) { - id = this._idCreator.getNextId(); + id = this._idGenerator.getNextId(); this._translationIdsDictionary[ originalString ] = id; } diff --git a/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js index 9b2a3ea69..31090c8b2 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js @@ -16,7 +16,6 @@ const { EventEmitter } = require( 'events' ); */ module.exports = class SingleLanguageTranslationService extends EventEmitter { /** - * * @param {String} language Target language. */ constructor( language ) { diff --git a/packages/ckeditor5-dev-webpack-plugin/tests/translatesourceloader.js b/packages/ckeditor5-dev-webpack-plugin/tests/translatesourceloader.js new file mode 100644 index 000000000..503ab589e --- /dev/null +++ b/packages/ckeditor5-dev-webpack-plugin/tests/translatesourceloader.js @@ -0,0 +1,36 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +const { expect } = require( 'chai' ); +const sinon = require( 'sinon' ); +const translateSourceLoader = require( '../lib/translatesourceloader' ); + +describe( 'webpack-plugin', () => { + describe( 'translateSourceLoader()', () => { + const sandbox = sinon.createSandbox(); + + afterEach( () => { + sandbox.restore(); + } ); + + it( 'should return translated code', () => { + const ctx = { + options: { + translateSource: sandbox.spy( () => 'output' ) + }, + resourcePath: 'file.js' + }; + + const result = translateSourceLoader.call( ctx, 'Source' ); + + sinon.assert.calledOnce( ctx.options.translateSource ); + sinon.assert.calledWithExactly( ctx.options.translateSource, 'Source', 'file.js' ); + + expect( result ).to.equal( 'output' ); + } ); + } ); +} ); From 0272f502a9bb013d8d6a3e94477359984983552a Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Wed, 15 Nov 2017 18:54:23 +0100 Subject: [PATCH 11/33] Added tests for `CKEditorWebpackPlugin`. --- .../tests/translations/findoriginalstrings.js | 3 +- .../ckeditor5-dev-webpack-plugin/lib/index.js | 6 + .../lib/servetranslations.js | 5 +- .../tests/index.js | 108 ++++++++++++++++++ .../tests/translatesourceloader.js | 34 +++--- 5 files changed, 133 insertions(+), 23 deletions(-) create mode 100644 packages/ckeditor5-dev-webpack-plugin/tests/index.js diff --git a/packages/ckeditor5-dev-utils/tests/translations/findoriginalstrings.js b/packages/ckeditor5-dev-utils/tests/translations/findoriginalstrings.js index 1008aa792..a0698b8db 100644 --- a/packages/ckeditor5-dev-utils/tests/translations/findoriginalstrings.js +++ b/packages/ckeditor5-dev-utils/tests/translations/findoriginalstrings.js @@ -5,9 +5,8 @@ 'use strict'; -const chai = require( 'chai' ); +const { expect } = require( 'chai' ); const sinon = require( 'sinon' ); -const expect = chai.expect; const proxyquire = require( 'proxyquire' ); describe( 'findOriginalStrings', () => { diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/index.js b/packages/ckeditor5-dev-webpack-plugin/lib/index.js index 85ff3d13c..0f5088cd4 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/index.js @@ -32,6 +32,12 @@ module.exports = class CKEditorWebpackPlugin { let translationService; + if ( this.options.languages.length === 0 ) { + throw new Error( chalk.red( + 'At least one target language should be specified.' + ) ); + } + if ( this.options.optimizeBuildForOneLanguage ) { if ( this.options.languages.length > 1 ) { throw new Error( chalk.red( diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js index 4d4292f6d..0bb10529e 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js @@ -6,8 +6,7 @@ const chalk = require( 'chalk' ); * Serve translations depending on the used translation service and passed options. * It takes care about whole Webpack compilation process. * - * See https://webpack.js.org/api/compiler/#event-hooks and https://webpack.js.org/api/compilation/ - * for details about specific hooks. + * See https://webpack.js.org/api/compiler/#event-hooks and https://webpack.js.org/api/compilation/ for details about specific hooks. * * @param {Object} compiler Webpack compiler. * @param {Object} options Translation options. @@ -16,7 +15,7 @@ const chalk = require( 'chalk' ); * should be relative to the webpack context. * @param {Boolean} [options.throwErrorOnMissingTranslation] Option that make this function throw when the translation is missing. * By default original (english translation keys) are used when the target translation is missing. - * @param {TranslationService} translationService + * @param {TranslationService} translationService Translation service that will load PO files, replace translation keys and generate assets. */ module.exports = function serveTranslations( compiler, options, translationService ) { const cwd = process.cwd(); diff --git a/packages/ckeditor5-dev-webpack-plugin/tests/index.js b/packages/ckeditor5-dev-webpack-plugin/tests/index.js new file mode 100644 index 000000000..e25ac6326 --- /dev/null +++ b/packages/ckeditor5-dev-webpack-plugin/tests/index.js @@ -0,0 +1,108 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +const { expect } = require( 'chai' ); +const sinon = require( 'sinon' ); +const proxyquire = require( 'proxyquire' ); +const SingleLanguageTranslationService = require( '@ckeditor/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice' ); +const MultipleLanguageTranslationService = require( '@ckeditor/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice' ); + +describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { + const sandbox = sinon.createSandbox(); + let CKEditorWebpackPlugin, stubs; + + beforeEach( () => { + stubs = { + serveTranslations: sandbox.spy() + }; + + CKEditorWebpackPlugin = proxyquire( '../lib/index', { + './servetranslations': stubs.serveTranslations + } ); + } ); + + afterEach( () => { + sandbox.restore(); + } ); + + describe( 'constructor()', () => { + it( 'should initialize with passed options', () => { + const options = { languages: [] }; + + const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( options ); + + expect( ckeditorWebpackPlugin.options ).to.equal( options ); + } ); + } ); + + describe( 'apply()', () => { + it( 'should throw if language array is empty', () => { + const options = { languages: [] }; + + const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( options ); + + expect( () => ckeditorWebpackPlugin.apply( {} ) ).to.throw( + 'At least one target language should be specified.' + ); + } ); + + it( 'should return and do nothing if language array is not specified', () => { + const options = {}; + + const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( options ); + ckeditorWebpackPlugin.apply( {} ); + + sinon.assert.notCalled( stubs.serveTranslations ); + } ); + + it( 'should throw an error when `optimizeBuildForOneLanguage` is enabled and multiple languages are selected.', () => { + const options = { + languages: [ 'pl', 'de' ], + optimizeBuildForOneLanguage: true + }; + + const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( options ); + + expect( () => ckeditorWebpackPlugin.apply( {} ) ).to.throw( + 'Only one language should be specified when `optimizeBuildForOneLanguage` option is on.' + ); + } ); + + it( 'should serve `SingleLanguageTranslationService` if the `optimizeBuildForOneLanguage` is enabled.', () => { + const options = { + languages: [ 'pl' ], + optimizeBuildForOneLanguage: true + }; + + const compiler = {}; + + const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( options ); + ckeditorWebpackPlugin.apply( compiler ); + + sinon.assert.calledOnce( stubs.serveTranslations ); + expect( stubs.serveTranslations.getCall( 0 ).args[ 0 ] ).to.equal( compiler ); + expect( stubs.serveTranslations.getCall( 0 ).args[ 1 ] ).to.equal( options ); + expect( stubs.serveTranslations.getCall( 0 ).args[ 2 ] ).to.be.instanceof( SingleLanguageTranslationService ); + } ); + + it( 'should serve `MultipleLanguageTranslationService` if the `optimizeBuildForOneLanguage` is disabled.', () => { + const options = { + languages: [ 'pl' ] + }; + + const compiler = {}; + + const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( options ); + ckeditorWebpackPlugin.apply( compiler ); + + sinon.assert.calledOnce( stubs.serveTranslations ); + expect( stubs.serveTranslations.getCall( 0 ).args[ 0 ] ).to.equal( compiler ); + expect( stubs.serveTranslations.getCall( 0 ).args[ 1 ] ).to.equal( options ); + expect( stubs.serveTranslations.getCall( 0 ).args[ 2 ] ).to.be.instanceof( MultipleLanguageTranslationService ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-dev-webpack-plugin/tests/translatesourceloader.js b/packages/ckeditor5-dev-webpack-plugin/tests/translatesourceloader.js index 503ab589e..d33251b1a 100644 --- a/packages/ckeditor5-dev-webpack-plugin/tests/translatesourceloader.js +++ b/packages/ckeditor5-dev-webpack-plugin/tests/translatesourceloader.js @@ -9,28 +9,26 @@ const { expect } = require( 'chai' ); const sinon = require( 'sinon' ); const translateSourceLoader = require( '../lib/translatesourceloader' ); -describe( 'webpack-plugin', () => { - describe( 'translateSourceLoader()', () => { - const sandbox = sinon.createSandbox(); +describe( 'webpack-plugin/translateSourceLoader()', () => { + const sandbox = sinon.createSandbox(); - afterEach( () => { - sandbox.restore(); - } ); + afterEach( () => { + sandbox.restore(); + } ); - it( 'should return translated code', () => { - const ctx = { - options: { - translateSource: sandbox.spy( () => 'output' ) - }, - resourcePath: 'file.js' - }; + it( 'should return translated code', () => { + const ctx = { + options: { + translateSource: sandbox.spy( () => 'output' ) + }, + resourcePath: 'file.js' + }; - const result = translateSourceLoader.call( ctx, 'Source' ); + const result = translateSourceLoader.call( ctx, 'Source' ); - sinon.assert.calledOnce( ctx.options.translateSource ); - sinon.assert.calledWithExactly( ctx.options.translateSource, 'Source', 'file.js' ); + sinon.assert.calledOnce( ctx.options.translateSource ); + sinon.assert.calledWithExactly( ctx.options.translateSource, 'Source', 'file.js' ); - expect( result ).to.equal( 'output' ); - } ); + expect( result ).to.equal( 'output' ); } ); } ); From 3e5785cb49f433a8d197eaee85e6931b416b65d8 Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Thu, 16 Nov 2017 14:58:47 +0100 Subject: [PATCH 12/33] Seperated ckeditor5-relative stuff from the main script. Added tests. --- .../lib/ckeditor5-env-utils.js | 68 +++++++ .../ckeditor5-dev-webpack-plugin/lib/index.js | 3 +- .../lib/servetranslations.js | 73 +++---- .../ckeditor5-dev-webpack-plugin/lib/utils.js | 12 -- .../tests/ckeditor5-env-utils.js | 180 ++++++++++++++++++ .../tests/index.js | 8 +- .../tests/utils.js | 78 -------- 7 files changed, 281 insertions(+), 141 deletions(-) create mode 100644 packages/ckeditor5-dev-webpack-plugin/lib/ckeditor5-env-utils.js delete mode 100644 packages/ckeditor5-dev-webpack-plugin/lib/utils.js create mode 100644 packages/ckeditor5-dev-webpack-plugin/tests/ckeditor5-env-utils.js delete mode 100644 packages/ckeditor5-dev-webpack-plugin/tests/utils.js diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/ckeditor5-env-utils.js b/packages/ckeditor5-dev-webpack-plugin/lib/ckeditor5-env-utils.js new file mode 100644 index 000000000..e83fb6ee6 --- /dev/null +++ b/packages/ckeditor5-dev-webpack-plugin/lib/ckeditor5-env-utils.js @@ -0,0 +1,68 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +const path = require( 'path' ); + +const CKEditor5CoreRegExp = /.+[/\\]ckeditor5-core/; +const CKEditor5PackageNameRegExp = /[/\\]ckeditor5-[^/\\]+[/\\]/; +const CKEditor5PackageSrcFileRegExp = /[/\\]ckeditor5-[^/\\]+[/\\]src[/\\].+\.js$/; + +/** + * Easily replaceable and testable set of CKEditor5 - related methods used by CKEditorWebpackPlugin internally. + */ +module.exports = { + loadCoreTranslations, + maybeLoadPackage, + maybeAddLoader +}; + +/** + * Resolve path to the core's translations and load them. + * + * @param {TranslationService} translationService + * @param {Object} resolver Webpack resolver that can resolve the resource's request. + */ +function loadCoreTranslations( cwd, translationService, resolver ) { + resolver.resolve( cwd, cwd, '@ckeditor/ckeditor5-core/src/editor/editor.js', ( err, result ) => { + const pathToCoreTranslationPackage = result.match( CKEditor5CoreRegExp )[ 0 ]; + + translationService.loadPackage( pathToCoreTranslationPackage ); + } ); +} + +/** + * Add package to the `TranslationService` if the resource comes from `ckeditor5-*` package. + * + * @param {TranslationService} translationService + * @param {String} resource Absolute path to the resource. + */ +function maybeLoadPackage( cwd, translationService, resource ) { + const relativePathToResource = path.relative( cwd, resource ); + + const match = relativePathToResource.match( CKEditor5PackageNameRegExp ); + + if ( match ) { + const index = relativePathToResource.search( CKEditor5PackageNameRegExp ) + match[ 0 ].length; + const pathToPackage = relativePathToResource.slice( 0, index ); + + translationService.loadPackage( pathToPackage ); + } +} + +/** + * Inject loader when the file comes from ckeditor5-* packages. + * + * @param {String} resource Absolute path to the resource. + * @param {Array.} loaders Array of Webpack loaders. + */ +function maybeAddLoader( cwd, resource, loaders ) { + const relativePathToResource = path.relative( cwd, resource ); + + if ( relativePathToResource.match( CKEditor5PackageSrcFileRegExp ) ) { + loaders.unshift( path.join( __dirname, 'translatesourceloader.js' ) ); + } +} diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/index.js b/packages/ckeditor5-dev-webpack-plugin/lib/index.js index 0f5088cd4..5f46ad3a3 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/index.js @@ -9,6 +9,7 @@ const chalk = require( 'chalk' ); const serveTranslations = require( './servetranslations' ); const SingleLanguageTranslationService = require( '@ckeditor/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice' ); const MultipleLanguageTranslationService = require( '@ckeditor/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice' ); +const ckeditor5EnvUtils = require( './ckeditor5-env-utils' ); module.exports = class CKEditorWebpackPlugin { /** @@ -56,6 +57,6 @@ module.exports = class CKEditorWebpackPlugin { translationService = new MultipleLanguageTranslationService( this.options.languages ); } - serveTranslations( compiler, this.options, translationService ); + serveTranslations( compiler, this.options, translationService, ckeditor5EnvUtils ); } }; diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js index 0bb10529e..cb2104068 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js @@ -1,5 +1,10 @@ -const utils = require( './utils' ); -const path = require( 'path' ); +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + const chalk = require( 'chalk' ); /** @@ -16,8 +21,9 @@ const chalk = require( 'chalk' ); * @param {Boolean} [options.throwErrorOnMissingTranslation] Option that make this function throw when the translation is missing. * By default original (english translation keys) are used when the target translation is missing. * @param {TranslationService} translationService Translation service that will load PO files, replace translation keys and generate assets. + * @param {Object} envUtils Environment utils that make it easy to test. */ -module.exports = function serveTranslations( compiler, options, translationService ) { +module.exports = function serveTranslations( compiler, options, translationService, envUtils ) { const cwd = process.cwd(); // Provides translateSource method for the `translatesourceloader` loader. @@ -32,67 +38,38 @@ module.exports = function serveTranslations( compiler, options, translationServi console.error( chalk.red( error ) ); } ); - // Add ckeditor5-core translations before `translatesourceloader` starts translating. + // Add core translations before `translatesourceloader` starts translating. compiler.plugin( 'after-resolvers', () => { - compiler.resolvers.normal.resolve( cwd, cwd, '@ckeditor/ckeditor5-core/src/editor/editor.js', ( err, result ) => { - const pathToCoreTranslationPackage = result.match( utils.CKEditor5CoreRegExp )[ 0 ]; + const resolver = compiler.resolvers.normal; - translationService.loadPackage( pathToCoreTranslationPackage ); - } ); + envUtils.loadCoreTranslations( cwd, translationService, resolver ); } ); + // Load translation files and add a loader if the package match requirements. compiler.plugin( 'normal-module-factory', nmf => { nmf.plugin( 'after-resolve', ( resolveOptions, done ) => { - maybeLoadPackage( resolveOptions ); - maybeAddLoader( resolveOptions ); + envUtils.maybeLoadPackage( cwd, translationService, resolveOptions.resource ); + envUtils.maybeAddLoader( cwd, resolveOptions.resource, resolveOptions.loaders ); done( null, resolveOptions ); } ); } ); + // At the end of the compilation add assets generated from the PO files. compiler.plugin( 'compilation', compilation => { compilation.plugin( 'additional-assets', done => { - addAssetsToExistingOnes( compilation.assets ); + const generatedAssets = translationService.getAssets( { outputDirectory: options.outputDirectory } ); + + for ( const asset of generatedAssets ) { + compilation.assets[ asset.outputPath ] = { + source: () => asset.outputBody, + size: () => asset.outputBody.length, + }; + } done(); } ); } ); - - // Add package to the translations if the resource comes from ckeditor5-* package. - function maybeLoadPackage( resolveOptions ) { - const packageNameRegExp = utils.CKEditor5PackageNameRegExp; - const relativePathToResource = path.relative( cwd, resolveOptions.resource ); - - const match = relativePathToResource.match( packageNameRegExp ); - - if ( match ) { - const index = relativePathToResource.search( packageNameRegExp ) + match[ 0 ].length; - const pathToPackage = relativePathToResource.slice( 0, index ); - - translationService.loadPackage( pathToPackage ); - } - } - - // Inject loader when the file comes from ckeditor5-* packages. - function maybeAddLoader( resolveOptions ) { - const relativePathToResource = path.relative( cwd, resolveOptions.resource ); - - if ( relativePathToResource.match( utils.CKEditor5PackageSrcFileRegExp ) ) { - resolveOptions.loaders.unshift( path.join( __dirname, 'translatesourceloader.js' ) ); - } - } - - // At the end add assets generated from the PO files. - function addAssetsToExistingOnes( destinationAssets ) { - const generatedAssets = translationService.getAssets( { outputDirectory: options.outputDirectory } ); - - for ( const asset of generatedAssets ) { - destinationAssets[ asset.outputPath ] = { - source: () => asset.outputBody, - size: () => asset.outputBody.length, - }; - } - } }; /** @@ -104,7 +81,7 @@ module.exports = function serveTranslations( compiler, options, translationServi */ /** - * Return assets + * Load package translations. * * @method #loadPackage * @param {String} pathToPackage Path to the package. diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/utils.js b/packages/ckeditor5-dev-webpack-plugin/lib/utils.js deleted file mode 100644 index 1dcee65ea..000000000 --- a/packages/ckeditor5-dev-webpack-plugin/lib/utils.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -module.exports = { - CKEditor5CoreRegExp: /.+[/\\]ckeditor5-core/, - CKEditor5PackageNameRegExp: /[/\\]ckeditor5-[^/\\]+[/\\]/, - CKEditor5PackageSrcFileRegExp: /[/\\]ckeditor5-[^/\\]+[/\\]src[/\\].+\.js$/ -}; diff --git a/packages/ckeditor5-dev-webpack-plugin/tests/ckeditor5-env-utils.js b/packages/ckeditor5-dev-webpack-plugin/tests/ckeditor5-env-utils.js new file mode 100644 index 000000000..096e7259f --- /dev/null +++ b/packages/ckeditor5-dev-webpack-plugin/tests/ckeditor5-env-utils.js @@ -0,0 +1,180 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +const { expect } = require( 'chai' ); +const sinon = require( 'sinon' ); +const originalPath = require( 'path' ); +const mockery = require( 'mockery' ); + +describe( 'webpack-plugin/ckeditor5-env-utils', () => { + const sandbox = sinon.createSandbox(); + let envUtils; + const path = {}; + + function useWindowsPaths() { + Object.assign( path, originalPath.win32 ); + } + + beforeEach( () => { + // Use posix by default. + Object.assign( path, originalPath.posix ); + } ); + + afterEach( () => { + sandbox.restore(); + } ); + + before( () => { + mockery.enable( { + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + } ); + + mockery.registerMock( 'path', path ); + envUtils = require( '../lib/ckeditor5-env-utils' ); + } ); + + after( () => { + mockery.disable(); + mockery.deregisterAll(); + } ); + + describe( 'loadCoreTranslations()', () => { + it( 'should load core translations', () => { + const resolver = { + resolve: ( context, requester, request, cb ) => { + cb( null, 'path/to/' + request ); + } + }; + + const translationService = { + loadPackage: sandbox.spy() + }; + + envUtils.loadCoreTranslations( 'cwd', translationService, resolver ); + + sinon.assert.calledOnce( translationService.loadPackage ); + sinon.assert.calledWithExactly( + translationService.loadPackage, + 'path/to/@ckeditor/ckeditor5-core' + ); + } ); + } ); + + describe( 'maybeLoadPackage()', () => { + it( 'should load package if the path match the regexp', () => { + const translationService = { + loadPackage: sandbox.spy() + }; + + envUtils.maybeLoadPackage( 'path', translationService, 'path/to/@ckeditor/ckeditor5-utils/src/util.js' ); + + sinon.assert.calledOnce( translationService.loadPackage ); + sinon.assert.calledWithExactly( translationService.loadPackage, 'to/@ckeditor/ckeditor5-utils/' ); + } ); + + it( 'should not load package if the path do not match the regexp', () => { + const translationService = { + loadPackage: sandbox.spy() + }; + + envUtils.maybeLoadPackage( 'path', translationService, 'path/to/@ckeditor/ckeditor5/src/util.js' ); + + sinon.assert.notCalled( translationService.loadPackage ); + } ); + + it( 'should work with Windows paths', () => { + useWindowsPaths(); + + const translationService = { + loadPackage: sandbox.spy() + }; + + envUtils.maybeLoadPackage( 'path', translationService, 'path\\to\\@ckeditor\\ckeditor5-utils\\src\\util.js' ); + + sinon.assert.calledOnce( translationService.loadPackage ); + sinon.assert.calledWithExactly( translationService.loadPackage, 'to\\@ckeditor\\ckeditor5-utils\\' ); + } ); + + it( 'should work with nested ckeditor5 packages', () => { + const translationService = { + loadPackage: sandbox.spy() + }; + + envUtils.maybeLoadPackage( + 'path/to/ckeditor5-build-classic', + translationService, + 'path/to/ckeditor5-build-classic/node_modules/@ckeditor/ckeditor5-utils/src/util.js' + ); + + sinon.assert.calledOnce( translationService.loadPackage ); + sinon.assert.calledWithExactly( translationService.loadPackage, 'node_modules/@ckeditor/ckeditor5-utils/' ); + } ); + } ); + + describe( 'maybeAddLoader()', () => { + it( 'should add a loader ot the resource if the resource\'s path match the RegExp on posix systems', () => { + const cwd = 'path'; + const resource = 'path/to/@ckeditor/ckeditor5-utils/src/util.js'; + const loaders = []; + + envUtils.maybeAddLoader( cwd, resource, loaders ); + + expect( loaders ).does.deep.equal( [ + originalPath.normalize( originalPath.join( __dirname, '../lib/translatesourceloader.js' ) ) + ] ); + } ); + + it( 'should add a loader ot the resource if the resource\'s path match the RegExp on the Windows systems', () => { + useWindowsPaths(); + + const cwd = 'path'; + const resource = 'path\\to\\@ckeditor\\ckeditor5-utils\\src\\util.js'; + const loaders = []; + + envUtils.maybeAddLoader( cwd, resource, loaders ); + + expect( loaders ).does.deep.equal( [ + path.normalize( path.join( __dirname, '..\\lib\\translatesourceloader.js' ) ) + ] ); + } ); + + it( 'should not add a loader ot the resource if the resource\'s path do not match the RegExp on posix systems', () => { + const cwd = 'path'; + const resource = 'path/to/@ckeditor/ckeditor5/src/util.js'; + const loaders = []; + + envUtils.maybeAddLoader( cwd, resource, loaders ); + + expect( loaders.length ).to.equal( 0 ); + } ); + + it( 'should work with nested ckeditor5 packages', () => { + const cwd = 'path/to/ckeditor5-build-classic'; + const resource = 'path/to/ckeditor5-build-classic/node_modules/@ckeditor/ckeditor5/src/util.js'; + const loaders = []; + + envUtils.maybeAddLoader( cwd, resource, loaders ); + + expect( loaders.length ).to.equal( 0 ); + } ); + + it( 'should work with nested ckeditor5 packages #2', () => { + const cwd = 'path/to/ckeditor5-build-classic'; + const resource = 'path/to/ckeditor5-build-classic/node_modules/@ckeditor/ckeditor5-utils/src/util.js'; + const loaders = []; + + envUtils.maybeAddLoader( cwd, resource, loaders ); + + expect( loaders ).does.deep.equal( [ + path.normalize( path.join( __dirname, '../lib/translatesourceloader.js' ) ) + ] ); + } ); + } ); +} ); + diff --git a/packages/ckeditor5-dev-webpack-plugin/tests/index.js b/packages/ckeditor5-dev-webpack-plugin/tests/index.js index e25ac6326..1c5f9858b 100644 --- a/packages/ckeditor5-dev-webpack-plugin/tests/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/tests/index.js @@ -17,11 +17,13 @@ describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { beforeEach( () => { stubs = { - serveTranslations: sandbox.spy() + serveTranslations: sandbox.spy(), + ckeditor5EnvUtils: {} }; CKEditorWebpackPlugin = proxyquire( '../lib/index', { - './servetranslations': stubs.serveTranslations + './servetranslations': stubs.serveTranslations, + './ckeditor5-env-utils': stubs.ckeditor5EnvUtils } ); } ); @@ -87,6 +89,7 @@ describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { expect( stubs.serveTranslations.getCall( 0 ).args[ 0 ] ).to.equal( compiler ); expect( stubs.serveTranslations.getCall( 0 ).args[ 1 ] ).to.equal( options ); expect( stubs.serveTranslations.getCall( 0 ).args[ 2 ] ).to.be.instanceof( SingleLanguageTranslationService ); + expect( stubs.serveTranslations.getCall( 0 ).args[ 3 ] ).to.equal( stubs.ckeditor5EnvUtils ); } ); it( 'should serve `MultipleLanguageTranslationService` if the `optimizeBuildForOneLanguage` is disabled.', () => { @@ -103,6 +106,7 @@ describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { expect( stubs.serveTranslations.getCall( 0 ).args[ 0 ] ).to.equal( compiler ); expect( stubs.serveTranslations.getCall( 0 ).args[ 1 ] ).to.equal( options ); expect( stubs.serveTranslations.getCall( 0 ).args[ 2 ] ).to.be.instanceof( MultipleLanguageTranslationService ); + expect( stubs.serveTranslations.getCall( 0 ).args[ 3 ] ).to.equal( stubs.ckeditor5EnvUtils ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-webpack-plugin/tests/utils.js b/packages/ckeditor5-dev-webpack-plugin/tests/utils.js deleted file mode 100644 index 82a4d1d5f..000000000 --- a/packages/ckeditor5-dev-webpack-plugin/tests/utils.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const { expect } = require( 'chai' ); -const utils = require( '../lib/utils.js' ); - -describe( 'webpack-plugin/utils', () => { - describe( 'CKEditor5CoreRegExp', () => { - it( 'should match CKEditor5 core package on Unix systems', () => { - const path = 'path/to/the/ckeditor5-core/src/file.js'; - - const match = path.match( utils.CKEditor5CoreRegExp ); - - expect( match ).to.not.be.null; - expect( match.length ).to.equal( 1 ); - expect( match[ 0 ] ).to.equal( 'path/to/the/ckeditor5-core' ); - } ); - - it( 'should match CKEditor5 core package on Windows systems', () => { - const path = 'C:\\some path\\to\\the\\ckeditor5-core\\src\\file.js'; - - const match = path.match( utils.CKEditor5CoreRegExp ); - - expect( match ).to.not.be.null; - expect( match.length ).to.equal( 1 ); - expect( match[ 0 ] ).to.equal( 'C:\\some path\\to\\the\\ckeditor5-core' ); - } ); - } ); - - describe( 'CKEditor5PackageNameRegExp', () => { - it( 'should match CKEditor5 package name on Unix systems', () => { - const path = 'path/to/the/ckeditor5-enter/src/file.js'; - - const match = path.match( utils.CKEditor5PackageNameRegExp ); - - expect( match ).to.not.be.null; - expect( match.length ).to.equal( 1 ); - expect( match[ 0 ] ).to.equal( '/ckeditor5-enter/' ); - } ); - - it( 'should match CKEditor5 package name on Windows systems', () => { - const path = 'C:\\some path\\to\\the\\ckeditor5-enter\\src\\file.js'; - - const match = path.match( utils.CKEditor5PackageNameRegExp ); - - expect( match ).to.not.be.null; - expect( match.length ).to.equal( 1 ); - expect( match[ 0 ] ).to.equal( '\\ckeditor5-enter\\' ); - } ); - } ); - - describe( 'CKEditor5PackageSrcFileRegExp', () => { - it( 'should match CKEditor5 package src file on Unix systems', () => { - const path = 'path/to/the/ckeditor5-enter/src/file.js'; - - const match = path.match( utils.CKEditor5PackageSrcFileRegExp ); - - expect( match ).to.not.be.null; - expect( match.length ).to.equal( 1 ); - expect( match[ 0 ] ).to.equal( '/ckeditor5-enter/src/file.js' ); - } ); - - it( 'should match CKEditor5 package src file on Windows systems', () => { - const path = 'C:\\some path\\to\\the\\ckeditor5-enter\\src\\file.js'; - - const match = path.match( utils.CKEditor5PackageSrcFileRegExp ); - - expect( match ).to.not.be.null; - expect( match.length ).to.equal( 1 ); - expect( match[ 0 ] ).to.equal( '\\ckeditor5-enter\\src\\file.js' ); - } ); - } ); -} ); - From a5b697af19ed8a3509c8337fbaa2f25d314b2aee Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Thu, 16 Nov 2017 15:18:12 +0100 Subject: [PATCH 13/33] Fixed error message and API docs. --- .../lib/translations/multiplelanguagetranslationservice.js | 2 +- .../lib/translations/singlelanguagetranslationservice.js | 2 +- .../tests/translations/multiplelanguagetranslationservice.js | 4 ++-- .../tests/translations/singlelanguagetranslationservice.js | 2 +- .../ckeditor5-dev-webpack-plugin/lib/servetranslations.js | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js index a65378a98..bc3375eef 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js @@ -118,7 +118,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { const translatedString = langDictionary[ originalString ]; if ( !translatedString ) { - this.emit( 'error', `Missing translation for ${ originalString } for ${ lang } language.` ); + this.emit( 'error', `Missing translation for '${ originalString }' for ${ lang } language.` ); } translatedStrings[ id ] = translatedString || originalString; diff --git a/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js index 31090c8b2..cd0c5ef7c 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js @@ -88,7 +88,7 @@ module.exports = class SingleLanguageTranslationService extends EventEmitter { _translateString( originalString, sourceFile ) { if ( !this._dictionary[ originalString ] ) { - this.emit( 'error', `Missing translation for ${ originalString } for ${ this._language } language in ${ sourceFile }.` ); + this.emit( 'error', `Missing translation for '${ originalString }' for ${ this._language } language in ${ sourceFile }.` ); return originalString; } diff --git a/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js index c936a9b23..fae114f52 100644 --- a/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js @@ -181,8 +181,8 @@ describe( 'translations', () => { sinon.assert.calledThrice( spy ); sinon.assert.calledWithExactly( spy, 'No translation found for xxx language.' ); - sinon.assert.calledWithExactly( spy, 'Missing translation for Cancel for xxx language.' ); - sinon.assert.calledWithExactly( spy, 'Missing translation for Save for xxx language.' ); + sinon.assert.calledWithExactly( spy, 'Missing translation for \'Cancel\' for xxx language.' ); + sinon.assert.calledWithExactly( spy, 'Missing translation for \'Save\' for xxx language.' ); } ); it( 'should feed missing translation with the translation key if the translated string is missing', () => { diff --git a/packages/ckeditor5-dev-utils/tests/translations/singlelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/tests/translations/singlelanguagetranslationservice.js index a22a9b707..946dbc6ab 100644 --- a/packages/ckeditor5-dev-utils/tests/translations/singlelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/tests/translations/singlelanguagetranslationservice.js @@ -116,7 +116,7 @@ describe( 'translations', () => { expect( result ).to.equal( 't(\'Cancel\');' ); sinon.assert.calledOnce( spy ); - sinon.assert.calledWithExactly( spy, 'Missing translation for Cancel for pl language in file.js.' ); + sinon.assert.calledWithExactly( spy, 'Missing translation for \'Cancel\' for pl language in file.js.' ); } ); it( 'should throw an error when the t is called with the variable', () => { diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js index cb2104068..b2ea16bbb 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js @@ -9,7 +9,7 @@ const chalk = require( 'chalk' ); /** * Serve translations depending on the used translation service and passed options. - * It takes care about whole Webpack compilation process. + * It takes care about whole Webpack compilation process and doesn't contain much logic that should be tested. * * See https://webpack.js.org/api/compiler/#event-hooks and https://webpack.js.org/api/compilation/ for details about specific hooks. * From c77d0215c1aca8ab19c2aa2d830be0fad1b2d08b Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Thu, 16 Nov 2017 17:31:00 +0100 Subject: [PATCH 14/33] Improved API docs. --- packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js index b2ea16bbb..8bda23a2a 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js @@ -21,7 +21,8 @@ const chalk = require( 'chalk' ); * @param {Boolean} [options.throwErrorOnMissingTranslation] Option that make this function throw when the translation is missing. * By default original (english translation keys) are used when the target translation is missing. * @param {TranslationService} translationService Translation service that will load PO files, replace translation keys and generate assets. - * @param {Object} envUtils Environment utils that make it easy to test. + * @param {Object} envUtils Environment utils internally called within the `serveTranslations()`, that make `serveTranslations()` + * ckeditor5 - independent without hard-to-test logic. */ module.exports = function serveTranslations( compiler, options, translationService, envUtils ) { const cwd = process.cwd(); From 3ea3783ba8d57ed9c28fdfe8adc8968a94e8d2c5 Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Wed, 22 Nov 2017 10:29:41 +0100 Subject: [PATCH 15/33] Added translations: `all` option. --- .../multiplelanguagetranslationservice.js | 39 ++++++++++++++++--- .../multiplelanguagetranslationservice.js | 8 ++-- .../ckeditor5-dev-webpack-plugin/lib/index.js | 19 +++++++-- 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js index bc3375eef..33ff57ed5 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js @@ -21,11 +21,14 @@ const { EventEmitter } = require( 'events' ); module.exports = class MultipleLanguageTranslationService extends EventEmitter { /** * @param {Array.} languages Target languages. + * @param {Boolean} compileAllLanguages Flag indicates whether the languages are specified or should be found at runtime. */ - constructor( languages ) { + constructor( languages, compileAllLanguages ) { super(); - this._languages = languages; + this._languages = new Set( languages ); + + this._compileAllLanguages = compileAllLanguages; this._packagePaths = new Set(); @@ -70,8 +73,32 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { this._packagePaths.add( pathToPackage ); + const pathToTranslationDirectory = this._getPathToTranslationDirectory( pathToPackage ); + + if ( this._compileAllLanguages ) { + if ( !fs.existsSync( pathToTranslationDirectory ) ) { + return; + } + + 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 = this._getPathToPoFile( pathToPackage, language ); + const pathToPoFile = path.join( pathToTranslationDirectory, language + '.po' ); this._loadPoFile( language, pathToPoFile ); } @@ -86,7 +113,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * @returns {Array.} */ getAssets( { outputDirectory = 'lang' } = {} ) { - return this._languages.map( language => { + return Array.from( this._languages ).map( language => { const translatedStrings = this._getIdToTranslatedStringDictionary( language ); const outputPath = path.join( outputDirectory, `${ language }.js` ); @@ -162,7 +189,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { /** * @protected */ - _getPathToPoFile( pathToPackage, languageCode ) { - return path.join( pathToPackage, 'lang', 'translations', languageCode + '.po' ); + _getPathToTranslationDirectory( pathToPackage ) { + return path.join( pathToPackage, 'lang', 'translations' ); } }; diff --git a/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js index fae114f52..70e6ed45e 100644 --- a/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js @@ -248,17 +248,17 @@ describe( 'translations', () => { } ); } ); - describe( '_getPathToPackage', () => { + describe( '_getPathToTranslationDirectory', () => { it( 'should be overridable to enable providing custom path to translation files', () => { class CustomTranslationService extends MultipleLanguageTranslationService { - _getPathToPoFile( pathToPackage, languageCode ) { - return path.join( 'custom', 'path', 'to', pathToPackage, languageCode + '.PO' ); + _getPathToTranslationDirectory( pathToPackage ) { + return path.join( 'custom', 'path', 'to', pathToPackage ); } } const translationService = new CustomTranslationService( [ 'en' ] ); - const pathToTranslations = path.join( 'custom', 'path', 'to', 'pathToPackage', 'en.PO' ); + const pathToTranslations = path.join( 'custom', 'path', 'to', 'pathToPackage', 'en.po' ); files = [ pathToTranslations ]; diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/index.js b/packages/ckeditor5-dev-webpack-plugin/lib/index.js index 5f46ad3a3..a91a53520 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/index.js @@ -32,15 +32,26 @@ module.exports = class CKEditorWebpackPlugin { } let translationService; + let compileAllLanguages = false; + let languages = this.options.languages; - if ( this.options.languages.length === 0 ) { + if ( typeof languages == 'string' ) { + if ( languages !== 'all' ) { + throw new Error( '`languages` option should be an array of language codes or `all`.' ); + } + + compileAllLanguages = true; + languages = []; // They will be searched in runtime. + } + + if ( languages.length === 0 && !compileAllLanguages ) { throw new Error( chalk.red( 'At least one target language should be specified.' ) ); } if ( this.options.optimizeBuildForOneLanguage ) { - if ( this.options.languages.length > 1 ) { + if ( languages.length > 1 || compileAllLanguages ) { throw new Error( chalk.red( 'Only one language should be specified when `optimizeBuildForOneLanguage` option is on.' ) ); @@ -52,9 +63,9 @@ module.exports = class CKEditorWebpackPlugin { ) ); } - translationService = new SingleLanguageTranslationService( this.options.languages[ 0 ] ); + translationService = new SingleLanguageTranslationService( languages[ 0 ] ); } else { - translationService = new MultipleLanguageTranslationService( this.options.languages ); + translationService = new MultipleLanguageTranslationService( languages, compileAllLanguages ); } serveTranslations( compiler, this.options, translationService, ckeditor5EnvUtils ); From bf04f26c3685640efe5ee12c1e116167a26f995a Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Wed, 22 Nov 2017 12:32:52 +0100 Subject: [PATCH 16/33] Added missing tests for the languages: `all` option. --- .../multiplelanguagetranslationservice.js | 4 +- .../multiplelanguagetranslationservice.js | 68 ++++++++++++++++--- .../tests/index.js | 17 +++++ 3 files changed, 76 insertions(+), 13 deletions(-) diff --git a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js index 33ff57ed5..4c37ab376 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js @@ -21,14 +21,14 @@ const { EventEmitter } = require( 'events' ); module.exports = class MultipleLanguageTranslationService extends EventEmitter { /** * @param {Array.} languages Target languages. - * @param {Boolean} compileAllLanguages Flag indicates whether the languages are specified or should be found at runtime. + * @param {Boolean} [compileAllLanguages] Flag indicates whether the languages are specified or should be found at runtime. */ constructor( languages, compileAllLanguages ) { super(); this._languages = new Set( languages ); - this._compileAllLanguages = compileAllLanguages; + this._compileAllLanguages = !!compileAllLanguages; this._packagePaths = new Set(); diff --git a/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js index 70e6ed45e..21b3bcc5b 100644 --- a/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js @@ -12,15 +12,19 @@ const proxyquire = require( 'proxyquire' ); describe( 'translations', () => { describe( 'MultipleLanguageTranslationService', () => { - let MultipleLanguageTranslationService, stubs, files, fileContents, sandbox; + let MultipleLanguageTranslationService, stubs, filesAndDirs, fileContents, dirContents; + const sandbox = sinon.sandbox.create(); beforeEach( () => { - sandbox = sinon.sandbox.create(); + filesAndDirs = []; + fileContents = {}; + dirContents = {}; stubs = { fs: { - existsSync: path => files.includes( path ), - readFileSync: path => fileContents[ path ] + existsSync: path => filesAndDirs.includes( path ), + readFileSync: path => fileContents[ path ], + readdirSync: dir => dirContents[ dir ] } }; @@ -42,12 +46,12 @@ describe( 'translations', () => { } ); describe( 'loadPackage()', () => { - it( 'should load po file from the package and load translations', () => { + it( 'should load PO file from the package and load translations', () => { const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); const pathToPlTranslations = path.join( 'pathToPackage', 'lang', 'translations', 'pl.po' ); const pathToDeTranslations = path.join( 'pathToPackage', 'lang', 'translations', 'de.po' ); - files = [ pathToPlTranslations, pathToDeTranslations ]; + filesAndDirs = [ pathToPlTranslations, pathToDeTranslations ]; fileContents = { [ pathToPlTranslations ]: [ @@ -76,10 +80,10 @@ describe( 'translations', () => { } ); } ); - it( 'should do nothing if the po file does not exist', () => { + it( 'should do nothing if the PO file does not exist', () => { const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); - files = []; + filesAndDirs = []; fileContents = {}; translationService.loadPackage( 'pathToPackage' ); @@ -87,7 +91,7 @@ describe( 'translations', () => { expect( translationService._dictionary ).to.deep.equal( {} ); } ); - it( 'should load po file from the package only once per language', () => { + it( 'should load PO file from the package only once per language', () => { const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); const loadPoFileSpy = sandbox.stub( translationService, '_loadPoFile' ); @@ -97,6 +101,48 @@ describe( 'translations', () => { sinon.assert.calledTwice( loadPoFileSpy ); } ); + + it( 'should load all PO files for the current package and add languages to the language list', () => { + const translationService = new MultipleLanguageTranslationService( [], true ); + + const pathToTranslations = path.join( 'pathToPackage', 'lang', 'translations' ); + const pathToPlTranslations = path.join( pathToTranslations, 'pl.po' ); + const pathToDeTranslations = path.join( pathToTranslations, 'de.po' ); + + filesAndDirs = [ pathToPlTranslations, pathToDeTranslations, pathToTranslations ]; + + fileContents = { + [ pathToPlTranslations ]: [ + 'msgctxt "Label for the Save button."', + 'msgid "Save"', + 'msgstr "Zapisz"', + '' + ].join( '\n' ), + [ pathToDeTranslations ]: [ + 'msgctxt "Label for the Save button."', + 'msgid "Save"', + 'msgstr "Speichern"', + '' + ].join( '\n' ) + }; + + dirContents = { + [ pathToTranslations ]: [ 'pl.po', 'de.po' ] + }; + + translationService.loadPackage( 'pathToPackage' ); + + expect( translationService._dictionary ).to.deep.equal( { + pl: { + 'Save': 'Zapisz' + }, + de: { + 'Save': 'Speichern' + } + } ); + + expect( Array.from( translationService._languages ) ).to.deep.equal( [ 'pl', 'de' ] ); + } ); } ); describe( 'translateSource()', () => { @@ -260,7 +306,7 @@ describe( 'translations', () => { const pathToTranslations = path.join( 'custom', 'path', 'to', 'pathToPackage', 'en.po' ); - files = [ pathToTranslations ]; + filesAndDirs = [ pathToTranslations ]; fileContents = { [ pathToTranslations ]: [ @@ -287,7 +333,7 @@ describe( 'translations', () => { const pathToPlTranslations = path.join( 'pathToPackage', 'lang', 'translations', 'pl.po' ); const pathToDeTranslations = path.join( 'pathToPackage', 'lang', 'translations', 'de.po' ); - files = [ pathToPlTranslations, pathToDeTranslations ]; + filesAndDirs = [ pathToPlTranslations, pathToDeTranslations ]; fileContents = { [ pathToPlTranslations ]: [ diff --git a/packages/ckeditor5-dev-webpack-plugin/tests/index.js b/packages/ckeditor5-dev-webpack-plugin/tests/index.js index 1c5f9858b..e50ac2758 100644 --- a/packages/ckeditor5-dev-webpack-plugin/tests/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/tests/index.js @@ -108,5 +108,22 @@ describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { expect( stubs.serveTranslations.getCall( 0 ).args[ 2 ] ).to.be.instanceof( MultipleLanguageTranslationService ); expect( stubs.serveTranslations.getCall( 0 ).args[ 3 ] ).to.equal( stubs.ckeditor5EnvUtils ); } ); + + it( 'should serve `MultipleLanguageTranslationService` if the `languages` is set to `all`.', () => { + const options = { + languages: 'all' + }; + + const compiler = {}; + + const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( options ); + ckeditorWebpackPlugin.apply( compiler ); + + sinon.assert.calledOnce( stubs.serveTranslations ); + expect( stubs.serveTranslations.getCall( 0 ).args[ 0 ] ).to.equal( compiler ); + expect( stubs.serveTranslations.getCall( 0 ).args[ 1 ] ).to.equal( options ); + expect( stubs.serveTranslations.getCall( 0 ).args[ 2 ] ).to.be.instanceof( MultipleLanguageTranslationService ); + expect( stubs.serveTranslations.getCall( 0 ).args[ 3 ] ).to.equal( stubs.ckeditor5EnvUtils ); + } ); } ); } ); From 232b5d239fc5286c3698a5d9300aee138ba40873 Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Wed, 22 Nov 2017 12:43:39 +0100 Subject: [PATCH 17/33] Added default param. --- .../lib/translations/multiplelanguagetranslationservice.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js index 4c37ab376..496da73d2 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js @@ -21,14 +21,14 @@ const { EventEmitter } = require( 'events' ); module.exports = class MultipleLanguageTranslationService extends EventEmitter { /** * @param {Array.} languages Target languages. - * @param {Boolean} [compileAllLanguages] Flag indicates whether the languages are specified or should be found at runtime. + * @param {Boolean} [compileAllLanguages=false] Flag indicates whether the languages are specified or should be found at runtime. */ - constructor( languages, compileAllLanguages ) { + constructor( languages, compileAllLanguages = false ) { super(); this._languages = new Set( languages ); - this._compileAllLanguages = !!compileAllLanguages; + this._compileAllLanguages = compileAllLanguages; this._packagePaths = new Set(); From c2b890348046f3e91026d5e93f62a6c0c062180d Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Thu, 23 Nov 2017 10:41:15 +0100 Subject: [PATCH 18/33] Enhanced TranlsationService. Made default language added to the main bundle. --- .../multiplelanguagetranslationservice.js | 50 +++++++++-- .../singlelanguagetranslationservice.js | 2 +- .../ckeditor5-dev-webpack-plugin/lib/index.js | 19 ++-- .../lib/servetranslations.js | 29 +++--- .../tests/index.js | 89 +++++++++++++------ 5 files changed, 130 insertions(+), 59 deletions(-) diff --git a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js index 496da73d2..a952496ae 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js @@ -21,13 +21,18 @@ const { EventEmitter } = require( 'events' ); module.exports = class MultipleLanguageTranslationService extends EventEmitter { /** * @param {Array.} languages Target languages. - * @param {Boolean} [compileAllLanguages=false] Flag indicates whether the languages are specified or should be found at runtime. + * @param {Object} options + * @param {Boolean} [options.compileAllLanguages=false] Flag indicates whether the languages are specified + * or should be found at runtime. + * @param {Boolean} [options.defaultLanguage] Default language that will be added to the main bundle (if possible). */ - constructor( languages, compileAllLanguages = false ) { + constructor( languages, { compileAllLanguages = false, defaultLanguage } = {} ) { super(); this._languages = new Set( languages ); + this._defaultLanguage = defaultLanguage || languages[ 0 ]; + this._compileAllLanguages = compileAllLanguages; this._packagePaths = new Set(); @@ -108,12 +113,45 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * Return an array of assets based on the stored dictionaries. * * @fires error - * @param {Object} param0 + * @param {Object} [param0] * @param {String} [param0.outputDirectory] + * @param {String} [param0.compilationAssets] * @returns {Array.} */ - getAssets( { outputDirectory = 'lang' } = {} ) { - return Array.from( this._languages ).map( language => { + getAssets( { outputDirectory = 'lang', compilationAssets } = {} ) { + const compilationAssetNames = Object.keys( compilationAssets ) + .filter( name => name.endsWith( '.js' ) ); + + if ( compilationAssetNames.length > 1 ) { + this.emit( 'error', [ + 'Because of the many found bundles, none bundle will contain the default language.', + `You should add it directly to the application from the '${ outputDirectory }/${ this._defaultLanguage }.js'.` + ].join( '\n' ) ); + + return this._getTranslationAssets( outputDirectory, this._languages ); + } + + const mainAssetName = compilationAssetNames[ 0 ]; + const mainCompilationAsset = compilationAssets[ mainAssetName ]; + + const mainTranslationAsset = this._getTranslationAssets( outputDirectory, [ this._defaultLanguage ] )[ 0 ]; + + const mergedCompilationAsset = { + outputBody: mainCompilationAsset.source() + '\n;' + mainTranslationAsset.outputBody, + outputPath: mainAssetName + }; + + const otherLanguages = Array.from( this._languages ) + .filter( lang => lang !== this._defaultLanguage ); + + return [ + mergedCompilationAsset, + ...this._getTranslationAssets( outputDirectory, otherLanguages ) + ]; + } + + _getTranslationAssets( outputDirectory, languages ) { + return Array.from( languages ).map( language => { const translatedStrings = this._getIdToTranslatedStringDictionary( language ); const outputPath = path.join( outputDirectory, `${ language }.js` ); @@ -122,7 +160,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { const outputBody = `CKEDITOR_TRANSLATIONS.add('${ language }',${ stringifiedTranslations })`; - return { outputPath, outputBody }; + return { outputBody, outputPath }; } ); } diff --git a/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js index cd0c5ef7c..8008d956c 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js @@ -48,7 +48,7 @@ module.exports = class SingleLanguageTranslationService extends EventEmitter { } /** - * Loads package and tries to get the po file from the package. + * Loads package and tries to get the PO file from the package. * * @param {String} pathToPackage Path to the package containing translations. */ diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/index.js b/packages/ckeditor5-dev-webpack-plugin/lib/index.js index a91a53520..3497d34aa 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/index.js @@ -14,11 +14,10 @@ const ckeditor5EnvUtils = require( './ckeditor5-env-utils' ); module.exports = class CKEditorWebpackPlugin { /** * @param {Object} [options] Plugin options. - * @param {Array.} [options.languages] Target languages. + * @param {Array.|'all'} [options.languages] Target languages. Build is optimized if only one language is provided. * @param {String} [options.outputDirectory='lang'] Output directory for the emitted translation files, * should be relative to the webpack context. - * @param {Boolean} [options.optimizeBuildForOneLanguage] Option that optimizes build for one language (directly replaces translation - * keys with the target language's strings. Webpack won't emit any language file with that option enabled. + * @param {String} [options.defaultLanguage] Default language for the build. * @param {Boolean} [options.throwErrorOnMissingTranslation] Option that make the plugin throw when the translation is missing. * By default original (english translation keys) are used when the target translation is missing. */ @@ -44,28 +43,24 @@ module.exports = class CKEditorWebpackPlugin { languages = []; // They will be searched in runtime. } + const defaultLanguage = this.options.defaultLanguage || languages[ 0 ]; + if ( languages.length === 0 && !compileAllLanguages ) { throw new Error( chalk.red( 'At least one target language should be specified.' ) ); } - if ( this.options.optimizeBuildForOneLanguage ) { - if ( languages.length > 1 || compileAllLanguages ) { - throw new Error( chalk.red( - 'Only one language should be specified when `optimizeBuildForOneLanguage` option is on.' - ) ); - } - + if ( languages.length === 1 ) { if ( this.options.outputDirectory ) { - console.error( chalk.red( + console.warn( chalk.red( '`outputDirectory` option does not work with `optimizeBuildForOneLanguage` option. It will be ignored.' ) ); } translationService = new SingleLanguageTranslationService( languages[ 0 ] ); } else { - translationService = new MultipleLanguageTranslationService( languages, compileAllLanguages ); + translationService = new MultipleLanguageTranslationService( languages, { compileAllLanguages, defaultLanguage } ); } serveTranslations( compiler, this.options, translationService, ckeditor5EnvUtils ); diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js index 8bda23a2a..7e0de7836 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js @@ -39,6 +39,10 @@ module.exports = function serveTranslations( compiler, options, translationServi console.error( chalk.red( error ) ); } ); + translationService.on( 'warning', warning => { + console.warn( chalk.yellow( warning ) ); + } ); + // Add core translations before `translatesourceloader` starts translating. compiler.plugin( 'after-resolvers', () => { const resolver = compiler.resolvers.normal; @@ -57,19 +61,20 @@ module.exports = function serveTranslations( compiler, options, translationServi } ); // At the end of the compilation add assets generated from the PO files. - compiler.plugin( 'compilation', compilation => { - compilation.plugin( 'additional-assets', done => { - const generatedAssets = translationService.getAssets( { outputDirectory: options.outputDirectory } ); - - for ( const asset of generatedAssets ) { - compilation.assets[ asset.outputPath ] = { - source: () => asset.outputBody, - size: () => asset.outputBody.length, - }; - } - - done(); + compiler.plugin( 'emit', ( compilation, done ) => { + const generatedAssets = translationService.getAssets( { + outputDirectory: options.outputDirectory, + compilationAssets: compilation.assets } ); + + for ( const asset of generatedAssets ) { + compilation.assets[ asset.outputPath ] = { + source: () => asset.outputBody, + size: () => asset.outputBody.length, + }; + } + + done(); } ); }; diff --git a/packages/ckeditor5-dev-webpack-plugin/tests/index.js b/packages/ckeditor5-dev-webpack-plugin/tests/index.js index e50ac2758..adf388427 100644 --- a/packages/ckeditor5-dev-webpack-plugin/tests/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/tests/index.js @@ -8,22 +8,33 @@ const { expect } = require( 'chai' ); const sinon = require( 'sinon' ); const proxyquire = require( 'proxyquire' ); -const SingleLanguageTranslationService = require( '@ckeditor/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice' ); -const MultipleLanguageTranslationService = require( '@ckeditor/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice' ); describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { const sandbox = sinon.createSandbox(); let CKEditorWebpackPlugin, stubs; beforeEach( () => { + class SingleLanguageTranslationService { + } + + class MultipleLanguageTranslationService { + } stubs = { serveTranslations: sandbox.spy(), - ckeditor5EnvUtils: {} + ckeditor5EnvUtils: {}, + SingleLanguageTranslationService: sandbox.spy( function() { + return sinon.createStubInstance( SingleLanguageTranslationService ); + } ), + MultipleLanguageTranslationService: sandbox.spy( function() { + return sinon.createStubInstance( MultipleLanguageTranslationService ); + } ) }; CKEditorWebpackPlugin = proxyquire( '../lib/index', { './servetranslations': stubs.serveTranslations, - './ckeditor5-env-utils': stubs.ckeditor5EnvUtils + './ckeditor5-env-utils': stubs.ckeditor5EnvUtils, + '@ckeditor/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice': stubs.SingleLanguageTranslationService, + '@ckeditor/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice': stubs.MultipleLanguageTranslationService } ); } ); @@ -61,23 +72,23 @@ describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { sinon.assert.notCalled( stubs.serveTranslations ); } ); - it( 'should throw an error when `optimizeBuildForOneLanguage` is enabled and multiple languages are selected.', () => { + it( 'should call serveTranslations() if the options are correct', () => { const options = { - languages: [ 'pl', 'de' ], - optimizeBuildForOneLanguage: true + languages: [ 'pl' ] }; + const compiler = {}; + const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( options ); + ckeditorWebpackPlugin.apply( compiler ); - expect( () => ckeditorWebpackPlugin.apply( {} ) ).to.throw( - 'Only one language should be specified when `optimizeBuildForOneLanguage` option is on.' - ); + sinon.assert.calledOnce( stubs.serveTranslations ); + sinon.assert.calledWith( stubs.serveTranslations, compiler, options ); } ); - it( 'should serve `SingleLanguageTranslationService` if the `optimizeBuildForOneLanguage` is enabled.', () => { + it( 'should serve `SingleLanguageTranslationService` if only one language is provided.', () => { const options = { - languages: [ 'pl' ], - optimizeBuildForOneLanguage: true + languages: [ 'pl' ] }; const compiler = {}; @@ -85,16 +96,13 @@ describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( options ); ckeditorWebpackPlugin.apply( compiler ); - sinon.assert.calledOnce( stubs.serveTranslations ); - expect( stubs.serveTranslations.getCall( 0 ).args[ 0 ] ).to.equal( compiler ); - expect( stubs.serveTranslations.getCall( 0 ).args[ 1 ] ).to.equal( options ); - expect( stubs.serveTranslations.getCall( 0 ).args[ 2 ] ).to.be.instanceof( SingleLanguageTranslationService ); - expect( stubs.serveTranslations.getCall( 0 ).args[ 3 ] ).to.equal( stubs.ckeditor5EnvUtils ); + sinon.assert.calledOnce( stubs.SingleLanguageTranslationService ); + sinon.assert.calledWithExactly( stubs.SingleLanguageTranslationService, 'pl' ); } ); - it( 'should serve `MultipleLanguageTranslationService` if the `optimizeBuildForOneLanguage` is disabled.', () => { + it( 'should serve `MultipleLanguageTranslationService` if more than 1 language is provided.', () => { const options = { - languages: [ 'pl' ] + languages: [ 'pl', 'en' ] }; const compiler = {}; @@ -103,10 +111,13 @@ describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { ckeditorWebpackPlugin.apply( compiler ); sinon.assert.calledOnce( stubs.serveTranslations ); - expect( stubs.serveTranslations.getCall( 0 ).args[ 0 ] ).to.equal( compiler ); - expect( stubs.serveTranslations.getCall( 0 ).args[ 1 ] ).to.equal( options ); - expect( stubs.serveTranslations.getCall( 0 ).args[ 2 ] ).to.be.instanceof( MultipleLanguageTranslationService ); - expect( stubs.serveTranslations.getCall( 0 ).args[ 3 ] ).to.equal( stubs.ckeditor5EnvUtils ); + + sinon.assert.calledOnce( stubs.MultipleLanguageTranslationService ); + sinon.assert.calledWithExactly( + stubs.MultipleLanguageTranslationService, + [ 'pl', 'en' ], + { compileAllLanguages: false, defaultLanguage: 'pl' } + ); } ); it( 'should serve `MultipleLanguageTranslationService` if the `languages` is set to `all`.', () => { @@ -120,10 +131,32 @@ describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { ckeditorWebpackPlugin.apply( compiler ); sinon.assert.calledOnce( stubs.serveTranslations ); - expect( stubs.serveTranslations.getCall( 0 ).args[ 0 ] ).to.equal( compiler ); - expect( stubs.serveTranslations.getCall( 0 ).args[ 1 ] ).to.equal( options ); - expect( stubs.serveTranslations.getCall( 0 ).args[ 2 ] ).to.be.instanceof( MultipleLanguageTranslationService ); - expect( stubs.serveTranslations.getCall( 0 ).args[ 3 ] ).to.equal( stubs.ckeditor5EnvUtils ); + + sinon.assert.calledOnce( stubs.MultipleLanguageTranslationService ); + sinon.assert.calledWithExactly( + stubs.MultipleLanguageTranslationService, + [], + { compileAllLanguages: true, defaultLanguage: undefined } + ); + } ); + + it( 'should set default language for `MultipleLanguageTranslationService` if provided with options', () => { + const options = { + languages: 'all', + defaultLanguage: 'de' + }; + + const compiler = {}; + + const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( options ); + ckeditorWebpackPlugin.apply( compiler ); + + sinon.assert.calledOnce( stubs.MultipleLanguageTranslationService ); + sinon.assert.calledWithExactly( + stubs.MultipleLanguageTranslationService, + [], + { compileAllLanguages: true, defaultLanguage: 'de' } + ); } ); } ); } ); From ba2d8e4abf5f471d2a9aa6eb63ac0befb32707c1 Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Thu, 23 Nov 2017 11:33:01 +0100 Subject: [PATCH 19/33] Fixed tests and added new ones. --- .../multiplelanguagetranslationservice.js | 26 ++-- .../multiplelanguagetranslationservice.js | 145 ++++++++++++++---- .../ckeditor5-dev-webpack-plugin/lib/index.js | 3 +- 3 files changed, 134 insertions(+), 40 deletions(-) diff --git a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js index a952496ae..84038514f 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js @@ -31,7 +31,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { this._languages = new Set( languages ); - this._defaultLanguage = defaultLanguage || languages[ 0 ]; + this._defaultLanguage = defaultLanguage; this._compileAllLanguages = compileAllLanguages; @@ -67,8 +67,10 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { } /** - * Load package and tries to get the po file from the package. + * Load package and tries to get PO files from the package if it's unknown. + * If the compileAllLanguages flag is set to true, language set will be enhanced by found languages. * + * @fires error * @param {String} pathToPackage Path to the package containing translations. */ loadPackage( pathToPackage ) { @@ -80,11 +82,11 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { const pathToTranslationDirectory = this._getPathToTranslationDirectory( pathToPackage ); - if ( this._compileAllLanguages ) { - if ( !fs.existsSync( pathToTranslationDirectory ) ) { - return; - } + 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.` ); @@ -113,19 +115,19 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * Return an array of assets based on the stored dictionaries. * * @fires error - * @param {Object} [param0] - * @param {String} [param0.outputDirectory] - * @param {String} [param0.compilationAssets] + * @param {Object} options + * @param {String} [options.outputDirectory] Output directory for the translation files relative to the output. + * @param {Object} options.compilationAssets Original assets from the compiler (e.g. Webpack). * @returns {Array.} */ - getAssets( { outputDirectory = 'lang', compilationAssets } = {} ) { + getAssets( { outputDirectory = 'lang', compilationAssets } ) { const compilationAssetNames = Object.keys( compilationAssets ) .filter( name => name.endsWith( '.js' ) ); if ( compilationAssetNames.length > 1 ) { - this.emit( 'error', [ + this.emit( 'warning', [ 'Because of the many found bundles, none bundle will contain the default language.', - `You should add it directly to the application from the '${ outputDirectory }/${ this._defaultLanguage }.js'.` + `You should add it directly to the application from the '${ outputDirectory }${ path.sep }${ this._defaultLanguage }.js'.` ].join( '\n' ) ); return this._getTranslationAssets( outputDirectory, this._languages ); diff --git a/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js index 21b3bcc5b..1ae4f2120 100644 --- a/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js @@ -48,10 +48,11 @@ describe( 'translations', () => { describe( 'loadPackage()', () => { it( 'should load PO file from the package and load translations', () => { const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); + const pathToTranslationsDirectory = path.join( 'pathToPackage', 'lang', 'translations' ); const pathToPlTranslations = path.join( 'pathToPackage', 'lang', 'translations', 'pl.po' ); const pathToDeTranslations = path.join( 'pathToPackage', 'lang', 'translations', 'de.po' ); - filesAndDirs = [ pathToPlTranslations, pathToDeTranslations ]; + filesAndDirs = [ pathToPlTranslations, pathToDeTranslations, pathToTranslationsDirectory ]; fileContents = { [ pathToPlTranslations ]: [ @@ -83,9 +84,6 @@ describe( 'translations', () => { it( 'should do nothing if the PO file does not exist', () => { const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); - filesAndDirs = []; - fileContents = {}; - translationService.loadPackage( 'pathToPackage' ); expect( translationService._dictionary ).to.deep.equal( {} ); @@ -95,6 +93,10 @@ describe( 'translations', () => { const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); const loadPoFileSpy = sandbox.stub( translationService, '_loadPoFile' ); + const pathToTranslationsDirectory = path.join( 'pathToPackage', 'lang', 'translations' ); + + filesAndDirs = [ pathToTranslationsDirectory ]; + translationService.loadPackage( 'pathToPackage' ); translationService.loadPackage( 'pathToPackage' ); translationService.loadPackage( 'pathToPackage' ); @@ -103,7 +105,7 @@ describe( 'translations', () => { } ); it( 'should load all PO files for the current package and add languages to the language list', () => { - const translationService = new MultipleLanguageTranslationService( [], true ); + const translationService = new MultipleLanguageTranslationService( [], { compileAllLanguages: true } ); const pathToTranslations = path.join( 'pathToPackage', 'lang', 'translations' ); const pathToPlTranslations = path.join( pathToTranslations, 'pl.po' ); @@ -173,7 +175,7 @@ describe( 'translations', () => { describe( 'getAssets()', () => { it( 'should return an array of assets', () => { - const translationService = new MultipleLanguageTranslationService( [ 'pl', 'en' ] ); + const translationService = new MultipleLanguageTranslationService( [ 'pl', 'en' ], { defaultLanguage: 'pl' } ); translationService._translationIdsDictionary = { Cancel: 'a', @@ -191,12 +193,16 @@ describe( 'translations', () => { } }; - const assets = translationService.getAssets(); + const assets = translationService.getAssets( { + compilationAssets: { + 'ckeditor.js': { source: () => 'source' } + } + } ); expect( assets ).to.deep.equal( [ { - outputPath: path.join( 'lang', 'pl.js' ), - outputBody: 'CKEDITOR_TRANSLATIONS.add(\'pl\',{a:"Anuluj",b:"Zapisz"})' + outputPath: 'ckeditor.js', + outputBody: 'source\n;CKEDITOR_TRANSLATIONS.add(\'pl\',{a:"Anuluj",b:"Zapisz"})' }, { outputPath: path.join( 'lang', 'en.js' ), @@ -206,7 +212,7 @@ describe( 'translations', () => { } ); it( 'should emit an error if the language is not present in language list', () => { - const translationService = new MultipleLanguageTranslationService( [ 'pl', 'xxx' ] ); + const translationService = new MultipleLanguageTranslationService( [ 'pl', 'xxx' ], { defaultLanguage: 'pl' } ); const spy = sandbox.spy(); translationService.on( 'error', spy ); @@ -223,7 +229,11 @@ describe( 'translations', () => { } }; - translationService.getAssets(); + translationService.getAssets( { + compilationAssets: { + 'ckeditor.js': { source: () => 'source' } + } + } ); sinon.assert.calledThrice( spy ); sinon.assert.calledWithExactly( spy, 'No translation found for xxx language.' ); @@ -232,7 +242,7 @@ describe( 'translations', () => { } ); it( 'should feed missing translation with the translation key if the translated string is missing', () => { - const translationService = new MultipleLanguageTranslationService( [ 'pl', 'xxx' ] ); + const translationService = new MultipleLanguageTranslationService( [ 'pl', 'xxx' ], { defaultLanguage: 'pl' } ); const spy = sandbox.spy(); translationService.on( 'error', spy ); @@ -249,12 +259,16 @@ describe( 'translations', () => { } }; - const assets = translationService.getAssets(); + const assets = translationService.getAssets( { + compilationAssets: { + 'ckeditor.js': { source: () => 'source' } + } + } ); expect( assets ).to.deep.equal( [ { - outputPath: path.join( 'lang', 'pl.js' ), - outputBody: 'CKEDITOR_TRANSLATIONS.add(\'pl\',{a:"Anuluj",b:"Zapisz"})' + outputPath: 'ckeditor.js', + outputBody: 'source\n;CKEDITOR_TRANSLATIONS.add(\'pl\',{a:"Anuluj",b:"Zapisz"})' }, { outputPath: path.join( 'lang', 'xxx.js' ), @@ -263,8 +277,8 @@ describe( 'translations', () => { ] ); } ); - it( 'should bound to assets only used translations', () => { - const translationService = new MultipleLanguageTranslationService( [ 'pl' ] ); + it( 'should emit an error if the main translation is missing', () => { + const translationService = new MultipleLanguageTranslationService( [ 'pl' ], { defaultLanguage: 'xxx' } ); const spy = sandbox.spy(); translationService.on( 'error', spy ); @@ -274,6 +288,33 @@ describe( 'translations', () => { Save: 'b' }; + translationService._dictionary = { + pl: { + Cancel: 'Anuluj', + Save: 'Zapisz', + } + }; + + translationService.getAssets( { + compilationAssets: { + 'ckeditor.js': { source: () => 'source' } + } + } ); + + sinon.assert.calledThrice( spy ); + sinon.assert.calledWithExactly( spy, 'No translation found for xxx language.' ); + sinon.assert.calledWithExactly( spy, 'Missing translation for \'Cancel\' for xxx language.' ); + sinon.assert.calledWithExactly( spy, 'Missing translation for \'Save\' for xxx language.' ); + } ); + + it( 'should bound to assets only used translations', () => { + const translationService = new MultipleLanguageTranslationService( [ 'pl' ], { defaultLanguage: 'pl' } ); + + translationService._translationIdsDictionary = { + Cancel: 'a', + Save: 'b' + }; + translationService._dictionary = { pl: { Cancel: 'Anuluj', @@ -282,9 +323,52 @@ describe( 'translations', () => { } }; - const assets = translationService.getAssets(); + const assets = translationService.getAssets( { + compilationAssets: { + 'ckeditor.js': { source: () => 'source' } + } + } ); // Note that the last translation from the above dictionary is skipped. + expect( assets ).to.deep.equal( [ + { + outputPath: 'ckeditor.js', + outputBody: 'source\n;CKEDITOR_TRANSLATIONS.add(\'pl\',{a:"Anuluj",b:"Zapisz"})' + } + ] ); + } ); + + it( 'should emit warning when many assets will be emitted by compilator and return only translation assets', () => { + const translationService = new MultipleLanguageTranslationService( [ 'pl' ], { defaultLanguage: 'pl' } ); + const spy = sandbox.spy(); + + translationService.on( 'warning', spy ); + + translationService._translationIdsDictionary = { + Cancel: 'a', + Save: 'b' + }; + + translationService._dictionary = { + pl: { + Cancel: 'Anuluj', + Save: 'Zapisz', + } + }; + + const assets = translationService.getAssets( { + compilationAssets: { + 'ckeditor.js': { source: () => 'source' }, + 'ckeditor2.js': { source: () => 'source' } + } + } ); + + sinon.assert.calledOnce( spy ); + sinon.assert.alwaysCalledWithExactly( spy, [ + 'Because of the many found bundles, none bundle will contain the default language.', + `You should add it directly to the application from the 'lang${ path.sep }pl.js'.` + ].join( '\n' ) ); + expect( assets ).to.deep.equal( [ { outputPath: path.join( 'lang', 'pl.js' ), @@ -302,14 +386,15 @@ describe( 'translations', () => { } } - const translationService = new CustomTranslationService( [ 'en' ] ); + const translationService = new CustomTranslationService( [ 'en' ], { defaultLanguage: 'en' } ); - const pathToTranslations = path.join( 'custom', 'path', 'to', 'pathToPackage', 'en.po' ); + const pathToPlTranslations = path.join( 'custom', 'path', 'to', 'pathToPackage', 'en.po' ); + const pathToTranslationDirectory = path.join( 'custom', 'path', 'to', 'pathToPackage' ); - filesAndDirs = [ pathToTranslations ]; + filesAndDirs = [ pathToPlTranslations, pathToTranslationDirectory ]; fileContents = { - [ pathToTranslations ]: [ + [ pathToPlTranslations ]: [ 'msgctxt "Label for the Save button."', 'msgid "Save"', 'msgstr "Save"', @@ -329,11 +414,12 @@ describe( 'translations', () => { describe( 'integration test', () => { it( 'test #1', () => { - const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); + const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ], { defaultLanguage: 'pl' } ); const pathToPlTranslations = path.join( 'pathToPackage', 'lang', 'translations', 'pl.po' ); const pathToDeTranslations = path.join( 'pathToPackage', 'lang', 'translations', 'de.po' ); + const pathToTranslationsDirectory = path.join( 'pathToPackage', 'lang', 'translations' ); - filesAndDirs = [ pathToPlTranslations, pathToDeTranslations ]; + filesAndDirs = [ pathToPlTranslations, pathToDeTranslations, pathToTranslationsDirectory ]; fileContents = { [ pathToPlTranslations ]: [ @@ -352,12 +438,17 @@ describe( 'translations', () => { translationService.loadPackage( 'pathToPackage' ); translationService.translateSource( 't( \'Save\' );' ); - const assets = translationService.getAssets(); + + const assets = translationService.getAssets( { + compilationAssets: { + 'ckeditor.js': { source: () => 'source' } + } + } ); expect( assets ).to.deep.equal( [ { - outputPath: path.join( 'lang', 'pl.js' ), - outputBody: 'CKEDITOR_TRANSLATIONS.add(\'pl\',{a:"Zapisz"})' + outputPath: 'ckeditor.js', + outputBody: 'source\n;CKEDITOR_TRANSLATIONS.add(\'pl\',{a:"Zapisz"})' }, { outputPath: path.join( 'lang', 'de.js' ), diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/index.js b/packages/ckeditor5-dev-webpack-plugin/lib/index.js index 3497d34aa..8fd5cbb10 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/index.js @@ -15,6 +15,7 @@ module.exports = class CKEditorWebpackPlugin { /** * @param {Object} [options] Plugin options. * @param {Array.|'all'} [options.languages] Target languages. Build is optimized if only one language is provided. + * When option is set to 'all' then script will be looking for all languages and according translations during the compilation. * @param {String} [options.outputDirectory='lang'] Output directory for the emitted translation files, * should be relative to the webpack context. * @param {String} [options.defaultLanguage] Default language for the build. @@ -54,7 +55,7 @@ module.exports = class CKEditorWebpackPlugin { if ( languages.length === 1 ) { if ( this.options.outputDirectory ) { console.warn( chalk.red( - '`outputDirectory` option does not work with `optimizeBuildForOneLanguage` option. It will be ignored.' + '`outputDirectory` option does not work for one language because zero files will be emitted. It will be ignored.' ) ); } From ba94e96367516dd493df16d6727fb4d6a235c11b Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Thu, 23 Nov 2017 11:58:28 +0100 Subject: [PATCH 20/33] Added missing test. --- .../multiplelanguagetranslationservice.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js index 1ae4f2120..32decd34d 100644 --- a/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js @@ -148,7 +148,7 @@ describe( 'translations', () => { } ); describe( 'translateSource()', () => { - it( 'should replace t() call params with the translation id, starting with `a`', () => { + it( 'should replace t() call params with the translation key, starting with `a`', () => { const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); const source = 't( \'Cancel\' ), t( \'Save\' );'; @@ -161,6 +161,18 @@ describe( 'translations', () => { } ); } ); + it( 'should not create new id for the same translation key', () => { + const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); + const source = 't( \'Cancel\' ), t( \'Cancel\' );'; + + const result = translationService.translateSource( source, 'file.js' ); + + expect( result ).to.equal( 't(\'a\'), t(\'a\');' ); + expect( translationService._translationIdsDictionary ).to.deep.equal( { + Cancel: 'a' + } ); + } ); + it( 'should return original source if there is no t() calls in the code', () => { const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); const source = 'translate( \'Cancel\' )'; From 006050890b9e63f499a1eef0561e6a61f8a280eb Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Fri, 24 Nov 2017 09:19:25 +0100 Subject: [PATCH 21/33] Added more API docs. --- .../multiplelanguagetranslationservice.js | 21 +++++++++++++------ .../ckeditor5-dev-webpack-plugin/lib/index.js | 15 +++++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js index 84038514f..d78bf1e5b 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js @@ -14,7 +14,7 @@ const { EventEmitter } = require( 'events' ); /** * `MultipleLanguageTranslationService` replaces `t()` call params with short ids - * and provides assets that translate those ids to target languages. + * and provides language assets that can translate those ids to the target languages. * * `translationKey` - original english string that occur in `t()` call params. */ @@ -35,7 +35,8 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { this._compileAllLanguages = compileAllLanguages; - this._packagePaths = new Set(); + // Set of handled packages that speed things up. + this._handledPackages = new Set(); // language -> translationKey -> targetTranslation dictionary. this._dictionary = {}; @@ -49,6 +50,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { /** * Translate file's source and replace `t()` call strings with short ids. + * Fire an error when the acorn parser face a trouble. * * @fires error * @param {String} source Source of the file. @@ -68,17 +70,17 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { /** * Load package and tries to get PO files from the package if it's unknown. - * If the compileAllLanguages flag is set to true, language set will be enhanced by found languages. + * If the `compileAllLanguages` flag is set to true, language's set will be expanded by the found languages. * * @fires error * @param {String} pathToPackage Path to the package containing translations. */ loadPackage( pathToPackage ) { - if ( this._packagePaths.has( pathToPackage ) ) { + if ( this._handledPackages.has( pathToPackage ) ) { return; } - this._packagePaths.add( pathToPackage ); + this._handledPackages.add( pathToPackage ); const pathToTranslationDirectory = this._getPathToTranslationDirectory( pathToPackage ); @@ -113,6 +115,8 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { /** * Return an array of assets based on the stored dictionaries. + * If there is one `compilationAssets`, merge main translation with that asset and join with other assets built outside. + * Otherwise fire an error and retuen an array of assets built outside of the `compilationAssets`. * * @fires error * @param {Object} options @@ -152,13 +156,16 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { ]; } + // Return assets for the given directory and languages. _getTranslationAssets( outputDirectory, languages ) { return Array.from( languages ).map( language => { const translatedStrings = this._getIdToTranslatedStringDictionary( language ); const outputPath = path.join( outputDirectory, `${ language }.js` ); + + // Stringify translations and remove unnecessary `""` around property names. const stringifiedTranslations = JSON.stringify( translatedStrings ) - .replace( /"([a-z]+)":/g, '$1:' ); // removes unnecessary `""` around property names. + .replace( /"([a-z]+)":/g, '$1:' ); const outputBody = `CKEDITOR_TRANSLATIONS.add('${ language }',${ stringifiedTranslations })`; @@ -227,6 +234,8 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { } /** + * Make this fn overridable, so the class might be used in other environments than CKE5. + * * @protected */ _getPathToTranslationDirectory( pathToPackage ) { diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/index.js b/packages/ckeditor5-dev-webpack-plugin/lib/index.js index 8fd5cbb10..51e6ca119 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/index.js @@ -11,6 +11,21 @@ const SingleLanguageTranslationService = require( '@ckeditor/ckeditor5-dev-utils const MultipleLanguageTranslationService = require( '@ckeditor/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice' ); const ckeditor5EnvUtils = require( './ckeditor5-env-utils' ); +/** + * CKEditorWebpackPlugin, for now, implements only the Translation Service (@ckeditor/ckeditor5#624, @ckeditor/ckeditor5#387). + * + * Workflow: + * + * One entry point (or to be precise one output JS file): + * - one language in `languages` -> build optimized version + * - many languages in `languages` –> get first or `defaultLanguage` as the main language that will be built into the main bundle + * (e.g. `ckeditor.js`) (`languages` must support `all` option, that's why the `defaultLanguage` option is needed) + * + * Multiple output JS files + * - one language in `languages` -> build optimized version + * - many languages in `languages` –> emit all translation files separately and warn user, + * that he needs to load translation file manually to get editor working + */ module.exports = class CKEditorWebpackPlugin { /** * @param {Object} [options] Plugin options. From e4b2c591970cb6916df48d47fe66d9ba8385a902 Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Fri, 24 Nov 2017 11:26:47 +0100 Subject: [PATCH 22/33] Added docs [skip ci]. --- .../tests/translations/multiplelanguagetranslationservice.js | 2 +- packages/ckeditor5-dev-webpack-plugin/lib/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js index 32decd34d..c7da4f8de 100644 --- a/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js @@ -377,7 +377,7 @@ describe( 'translations', () => { sinon.assert.calledOnce( spy ); sinon.assert.alwaysCalledWithExactly( spy, [ - 'Because of the many found bundles, none bundle will contain the default language.', + 'Because of the many found bundles, none of the bundles will contain the default language.', `You should add it directly to the application from the 'lang${ path.sep }pl.js'.` ].join( '\n' ) ); diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/index.js b/packages/ckeditor5-dev-webpack-plugin/lib/index.js index 51e6ca119..3e7609362 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/index.js @@ -33,7 +33,7 @@ module.exports = class CKEditorWebpackPlugin { * When option is set to 'all' then script will be looking for all languages and according translations during the compilation. * @param {String} [options.outputDirectory='lang'] Output directory for the emitted translation files, * should be relative to the webpack context. - * @param {String} [options.defaultLanguage] Default language for the build. + * @param {String} [options.defaultLanguage] Default language for the build. If not set, the first language will be used. * @param {Boolean} [options.throwErrorOnMissingTranslation] Option that make the plugin throw when the translation is missing. * By default original (english translation keys) are used when the target translation is missing. */ From 356cfabc26c99e3caa1eead62dd4e83a0ce4e23d Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Fri, 24 Nov 2017 12:38:22 +0100 Subject: [PATCH 23/33] Changed options to `language` and `additionalLanguages`. --- .../multiplelanguagetranslationservice.js | 19 +++---- .../multiplelanguagetranslationservice.js | 36 +++++++------ .../ckeditor5-dev-webpack-plugin/lib/index.js | 46 +++++++--------- .../tests/index.js | 53 +++++-------------- 4 files changed, 62 insertions(+), 92 deletions(-) diff --git a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js index d78bf1e5b..78e33f82a 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js @@ -20,18 +20,19 @@ const { EventEmitter } = require( 'events' ); */ module.exports = class MultipleLanguageTranslationService extends EventEmitter { /** - * @param {Array.} languages Target languages. + * @param {String} language Main language. * @param {Object} options * @param {Boolean} [options.compileAllLanguages=false] Flag indicates whether the languages are specified * or should be found at runtime. - * @param {Boolean} [options.defaultLanguage] Default language that will be added to the main bundle (if possible). + * @param {Array.} options.additionalLanguages Additional languages. Build is optimized for this option is not set. + * When option is set to 'all' then script will be looking for all languages and according translations during the compilation. */ - constructor( languages, { compileAllLanguages = false, defaultLanguage } = {} ) { + constructor( language, { additionalLanguages, compileAllLanguages = false } = {} ) { super(); - this._languages = new Set( languages ); + this._mainLanguage = language; - this._defaultLanguage = defaultLanguage; + this._languages = new Set( [ language, ...additionalLanguages ] ); this._compileAllLanguages = compileAllLanguages; @@ -130,8 +131,8 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { if ( compilationAssetNames.length > 1 ) { this.emit( 'warning', [ - 'Because of the many found bundles, none bundle will contain the default language.', - `You should add it directly to the application from the '${ outputDirectory }${ path.sep }${ this._defaultLanguage }.js'.` + 'Because of the many found bundles, none of the bundles will contain the main language.', + `You should add it directly to the application from the '${ outputDirectory }${ path.sep }${ this._mainLanguage }.js'.` ].join( '\n' ) ); return this._getTranslationAssets( outputDirectory, this._languages ); @@ -140,7 +141,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { const mainAssetName = compilationAssetNames[ 0 ]; const mainCompilationAsset = compilationAssets[ mainAssetName ]; - const mainTranslationAsset = this._getTranslationAssets( outputDirectory, [ this._defaultLanguage ] )[ 0 ]; + const mainTranslationAsset = this._getTranslationAssets( outputDirectory, [ this._mainLanguage ] )[ 0 ]; const mergedCompilationAsset = { outputBody: mainCompilationAsset.source() + '\n;' + mainTranslationAsset.outputBody, @@ -148,7 +149,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { }; const otherLanguages = Array.from( this._languages ) - .filter( lang => lang !== this._defaultLanguage ); + .filter( lang => lang !== this._mainLanguage ); return [ mergedCompilationAsset, diff --git a/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js index c7da4f8de..73ab2c026 100644 --- a/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js @@ -39,7 +39,7 @@ describe( 'translations', () => { describe( 'constructor()', () => { it( 'should initialize `SingleLanguageTranslationService`', () => { - const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); + const translationService = new MultipleLanguageTranslationService( 'en', { additionalLanguages: [ 'pl', 'de' ] } ); expect( translationService ).to.be.instanceof( MultipleLanguageTranslationService ); } ); @@ -47,7 +47,7 @@ describe( 'translations', () => { describe( 'loadPackage()', () => { it( 'should load PO file from the package and load translations', () => { - const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); + const translationService = new MultipleLanguageTranslationService( 'pl', { additionalLanguages: [ 'de' ] } ); const pathToTranslationsDirectory = path.join( 'pathToPackage', 'lang', 'translations' ); const pathToPlTranslations = path.join( 'pathToPackage', 'lang', 'translations', 'pl.po' ); const pathToDeTranslations = path.join( 'pathToPackage', 'lang', 'translations', 'de.po' ); @@ -82,7 +82,7 @@ describe( 'translations', () => { } ); it( 'should do nothing if the PO file does not exist', () => { - const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); + const translationService = new MultipleLanguageTranslationService( 'pl', { additionalLanguages: [ 'de' ] } ); translationService.loadPackage( 'pathToPackage' ); @@ -90,7 +90,7 @@ describe( 'translations', () => { } ); it( 'should load PO file from the package only once per language', () => { - const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); + const translationService = new MultipleLanguageTranslationService( 'pl', { additionalLanguages: [ 'de' ] } ); const loadPoFileSpy = sandbox.stub( translationService, '_loadPoFile' ); const pathToTranslationsDirectory = path.join( 'pathToPackage', 'lang', 'translations' ); @@ -105,7 +105,9 @@ describe( 'translations', () => { } ); it( 'should load all PO files for the current package and add languages to the language list', () => { - const translationService = new MultipleLanguageTranslationService( [], { compileAllLanguages: true } ); + const translationService = new MultipleLanguageTranslationService( 'pl', { + compileAllLanguages: true, additionalLanguages: [] + } ); const pathToTranslations = path.join( 'pathToPackage', 'lang', 'translations' ); const pathToPlTranslations = path.join( pathToTranslations, 'pl.po' ); @@ -149,7 +151,7 @@ describe( 'translations', () => { describe( 'translateSource()', () => { it( 'should replace t() call params with the translation key, starting with `a`', () => { - const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); + const translationService = new MultipleLanguageTranslationService( 'pl', { additionalLanguages: [ 'de' ] } ); const source = 't( \'Cancel\' ), t( \'Save\' );'; const result = translationService.translateSource( source, 'file.js' ); @@ -162,7 +164,7 @@ describe( 'translations', () => { } ); it( 'should not create new id for the same translation key', () => { - const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); + const translationService = new MultipleLanguageTranslationService( 'pl', { additionalLanguages: [ 'de' ] } ); const source = 't( \'Cancel\' ), t( \'Cancel\' );'; const result = translationService.translateSource( source, 'file.js' ); @@ -174,7 +176,7 @@ describe( 'translations', () => { } ); it( 'should return original source if there is no t() calls in the code', () => { - const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ] ); + const translationService = new MultipleLanguageTranslationService( 'pl', { additionalLanguages: [ 'de' ] } ); const source = 'translate( \'Cancel\' )'; const result = translationService.translateSource( source, 'file.js' ); @@ -187,7 +189,7 @@ describe( 'translations', () => { describe( 'getAssets()', () => { it( 'should return an array of assets', () => { - const translationService = new MultipleLanguageTranslationService( [ 'pl', 'en' ], { defaultLanguage: 'pl' } ); + const translationService = new MultipleLanguageTranslationService( 'pl', { additionalLanguages: [ 'en' ] } ); translationService._translationIdsDictionary = { Cancel: 'a', @@ -224,7 +226,7 @@ describe( 'translations', () => { } ); it( 'should emit an error if the language is not present in language list', () => { - const translationService = new MultipleLanguageTranslationService( [ 'pl', 'xxx' ], { defaultLanguage: 'pl' } ); + const translationService = new MultipleLanguageTranslationService( 'pl', { additionalLanguages: [ 'xxx' ] } ); const spy = sandbox.spy(); translationService.on( 'error', spy ); @@ -254,7 +256,7 @@ describe( 'translations', () => { } ); it( 'should feed missing translation with the translation key if the translated string is missing', () => { - const translationService = new MultipleLanguageTranslationService( [ 'pl', 'xxx' ], { defaultLanguage: 'pl' } ); + const translationService = new MultipleLanguageTranslationService( 'pl', { additionalLanguages: [ 'xxx' ] } ); const spy = sandbox.spy(); translationService.on( 'error', spy ); @@ -290,7 +292,7 @@ describe( 'translations', () => { } ); it( 'should emit an error if the main translation is missing', () => { - const translationService = new MultipleLanguageTranslationService( [ 'pl' ], { defaultLanguage: 'xxx' } ); + const translationService = new MultipleLanguageTranslationService( 'xxx', { additionalLanguages: [ 'pl' ] } ); const spy = sandbox.spy(); translationService.on( 'error', spy ); @@ -320,7 +322,7 @@ describe( 'translations', () => { } ); it( 'should bound to assets only used translations', () => { - const translationService = new MultipleLanguageTranslationService( [ 'pl' ], { defaultLanguage: 'pl' } ); + const translationService = new MultipleLanguageTranslationService( 'pl', { additionalLanguages: [] } ); translationService._translationIdsDictionary = { Cancel: 'a', @@ -351,7 +353,7 @@ describe( 'translations', () => { } ); it( 'should emit warning when many assets will be emitted by compilator and return only translation assets', () => { - const translationService = new MultipleLanguageTranslationService( [ 'pl' ], { defaultLanguage: 'pl' } ); + const translationService = new MultipleLanguageTranslationService( 'pl', { additionalLanguages: [] } ); const spy = sandbox.spy(); translationService.on( 'warning', spy ); @@ -377,7 +379,7 @@ describe( 'translations', () => { sinon.assert.calledOnce( spy ); sinon.assert.alwaysCalledWithExactly( spy, [ - 'Because of the many found bundles, none of the bundles will contain the default language.', + 'Because of the many found bundles, none of the bundles will contain the main language.', `You should add it directly to the application from the 'lang${ path.sep }pl.js'.` ].join( '\n' ) ); @@ -398,7 +400,7 @@ describe( 'translations', () => { } } - const translationService = new CustomTranslationService( [ 'en' ], { defaultLanguage: 'en' } ); + const translationService = new CustomTranslationService( 'en', { additionalLanguages: [] } ); const pathToPlTranslations = path.join( 'custom', 'path', 'to', 'pathToPackage', 'en.po' ); const pathToTranslationDirectory = path.join( 'custom', 'path', 'to', 'pathToPackage' ); @@ -426,7 +428,7 @@ describe( 'translations', () => { describe( 'integration test', () => { it( 'test #1', () => { - const translationService = new MultipleLanguageTranslationService( [ 'pl', 'de' ], { defaultLanguage: 'pl' } ); + const translationService = new MultipleLanguageTranslationService( 'pl', { additionalLanguages: [ 'de' ] } ); const pathToPlTranslations = path.join( 'pathToPackage', 'lang', 'translations', 'pl.po' ); const pathToDeTranslations = path.join( 'pathToPackage', 'lang', 'translations', 'de.po' ); const pathToTranslationsDirectory = path.join( 'pathToPackage', 'lang', 'translations' ); diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/index.js b/packages/ckeditor5-dev-webpack-plugin/lib/index.js index 3e7609362..fbaf74f86 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/index.js @@ -17,23 +17,24 @@ const ckeditor5EnvUtils = require( './ckeditor5-env-utils' ); * Workflow: * * One entry point (or to be precise one output JS file): - * - one language in `languages` -> build optimized version - * - many languages in `languages` –> get first or `defaultLanguage` as the main language that will be built into the main bundle - * (e.g. `ckeditor.js`) (`languages` must support `all` option, that's why the `defaultLanguage` option is needed) + * - `additinalLanguages` not set -> build optimized version + * - `additinalLanguages` set –> `language` will be built into the main bundle (e.g. `ckeditor.js`). + * Translation files will be emitted in the `outputDirectory` or 'lang' directory. * * Multiple output JS files - * - one language in `languages` -> build optimized version - * - many languages in `languages` –> emit all translation files separately and warn user, - * that he needs to load translation file manually to get editor working + * - `additinalLanguages` not set -> build optimized version + * - `additinalLanguages` set –> emit all translation files separately and warn user, + * that he needs to load at least one translation file manually to get editor working */ module.exports = class CKEditorWebpackPlugin { /** * @param {Object} [options] Plugin options. - * @param {Array.|'all'} [options.languages] Target languages. Build is optimized if only one language is provided. - * When option is set to 'all' then script will be looking for all languages and according translations during the compilation. + * @param {String} options.language Main language of the build that will be added to the bundle. + * @param {Array.|'all'} [options.additionalLanguages] Additional languages. Build is optimized when this option is not set. + * When `additionalLanguages` is set to 'all' then script will be looking for all languages and according translations during + * the compilation. * @param {String} [options.outputDirectory='lang'] Output directory for the emitted translation files, * should be relative to the webpack context. - * @param {String} [options.defaultLanguage] Default language for the build. If not set, the first language will be used. * @param {Boolean} [options.throwErrorOnMissingTranslation] Option that make the plugin throw when the translation is missing. * By default original (english translation keys) are used when the target translation is missing. */ @@ -42,41 +43,34 @@ module.exports = class CKEditorWebpackPlugin { } apply( compiler ) { - if ( !this.options.languages ) { + if ( !this.options.language ) { return; } + const language = this.options.language; let translationService; let compileAllLanguages = false; - let languages = this.options.languages; + let additionalLanguages = this.options.additionalLanguages; - if ( typeof languages == 'string' ) { - if ( languages !== 'all' ) { - throw new Error( '`languages` option should be an array of language codes or `all`.' ); + if ( typeof additionalLanguages == 'string' ) { + if ( additionalLanguages !== 'all' ) { + throw new Error( '`additinalLanguages` option should be an array of language codes or `all`.' ); } compileAllLanguages = true; - languages = []; // They will be searched in runtime. + additionalLanguages = []; // They will be searched in runtime. } - const defaultLanguage = this.options.defaultLanguage || languages[ 0 ]; - - if ( languages.length === 0 && !compileAllLanguages ) { - throw new Error( chalk.red( - 'At least one target language should be specified.' - ) ); - } - - if ( languages.length === 1 ) { + if ( !additionalLanguages ) { if ( this.options.outputDirectory ) { console.warn( chalk.red( '`outputDirectory` option does not work for one language because zero files will be emitted. It will be ignored.' ) ); } - translationService = new SingleLanguageTranslationService( languages[ 0 ] ); + translationService = new SingleLanguageTranslationService( language ); } else { - translationService = new MultipleLanguageTranslationService( languages, { compileAllLanguages, defaultLanguage } ); + translationService = new MultipleLanguageTranslationService( language, { compileAllLanguages, additionalLanguages } ); } serveTranslations( compiler, this.options, translationService, ckeditor5EnvUtils ); diff --git a/packages/ckeditor5-dev-webpack-plugin/tests/index.js b/packages/ckeditor5-dev-webpack-plugin/tests/index.js index adf388427..b9a4344be 100644 --- a/packages/ckeditor5-dev-webpack-plugin/tests/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/tests/index.js @@ -44,7 +44,7 @@ describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { describe( 'constructor()', () => { it( 'should initialize with passed options', () => { - const options = { languages: [] }; + const options = { language: 'pl' }; const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( options ); @@ -53,17 +53,7 @@ describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { } ); describe( 'apply()', () => { - it( 'should throw if language array is empty', () => { - const options = { languages: [] }; - - const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( options ); - - expect( () => ckeditorWebpackPlugin.apply( {} ) ).to.throw( - 'At least one target language should be specified.' - ); - } ); - - it( 'should return and do nothing if language array is not specified', () => { + it( 'should return and do nothing if language is not specified', () => { const options = {}; const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( options ); @@ -74,7 +64,7 @@ describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { it( 'should call serveTranslations() if the options are correct', () => { const options = { - languages: [ 'pl' ] + language: 'pl' }; const compiler = {}; @@ -88,7 +78,7 @@ describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { it( 'should serve `SingleLanguageTranslationService` if only one language is provided.', () => { const options = { - languages: [ 'pl' ] + language: 'pl' }; const compiler = {}; @@ -102,7 +92,8 @@ describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { it( 'should serve `MultipleLanguageTranslationService` if more than 1 language is provided.', () => { const options = { - languages: [ 'pl', 'en' ] + language: 'pl', + additionalLanguages: [ 'en' ] }; const compiler = {}; @@ -115,14 +106,15 @@ describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { sinon.assert.calledOnce( stubs.MultipleLanguageTranslationService ); sinon.assert.calledWithExactly( stubs.MultipleLanguageTranslationService, - [ 'pl', 'en' ], - { compileAllLanguages: false, defaultLanguage: 'pl' } + 'pl', + { compileAllLanguages: false, additionalLanguages: [ 'en' ] } ); } ); - it( 'should serve `MultipleLanguageTranslationService` if the `languages` is set to `all`.', () => { + it( 'should serve `MultipleLanguageTranslationService` if the `additionalLanguages` is set to `all`.', () => { const options = { - languages: 'all' + language: 'en', + additionalLanguages: 'all' }; const compiler = {}; @@ -135,27 +127,8 @@ describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { sinon.assert.calledOnce( stubs.MultipleLanguageTranslationService ); sinon.assert.calledWithExactly( stubs.MultipleLanguageTranslationService, - [], - { compileAllLanguages: true, defaultLanguage: undefined } - ); - } ); - - it( 'should set default language for `MultipleLanguageTranslationService` if provided with options', () => { - const options = { - languages: 'all', - defaultLanguage: 'de' - }; - - const compiler = {}; - - const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( options ); - ckeditorWebpackPlugin.apply( compiler ); - - sinon.assert.calledOnce( stubs.MultipleLanguageTranslationService ); - sinon.assert.calledWithExactly( - stubs.MultipleLanguageTranslationService, - [], - { compileAllLanguages: true, defaultLanguage: 'de' } + 'en', + { compileAllLanguages: true, additionalLanguages: [] } ); } ); } ); From 498e6a9175231ecfeca06247df8a0e2bc95c0811 Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Fri, 24 Nov 2017 13:16:43 +0100 Subject: [PATCH 24/33] Fixed typos. --- packages/ckeditor5-dev-webpack-plugin/lib/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/index.js b/packages/ckeditor5-dev-webpack-plugin/lib/index.js index fbaf74f86..e255f288e 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/index.js @@ -17,13 +17,13 @@ const ckeditor5EnvUtils = require( './ckeditor5-env-utils' ); * Workflow: * * One entry point (or to be precise one output JS file): - * - `additinalLanguages` not set -> build optimized version - * - `additinalLanguages` set –> `language` will be built into the main bundle (e.g. `ckeditor.js`). + * - `additionalLanguages` not set -> build optimized version + * - `additionalLanguages` set –> `language` will be built into the main bundle (e.g. `ckeditor.js`). * Translation files will be emitted in the `outputDirectory` or 'lang' directory. * * Multiple output JS files - * - `additinalLanguages` not set -> build optimized version - * - `additinalLanguages` set –> emit all translation files separately and warn user, + * - `additionalLanguages` not set -> build optimized version + * - `additionalLanguages` set –> emit all translation files separately and warn user, * that he needs to load at least one translation file manually to get editor working */ module.exports = class CKEditorWebpackPlugin { @@ -54,7 +54,7 @@ module.exports = class CKEditorWebpackPlugin { if ( typeof additionalLanguages == 'string' ) { if ( additionalLanguages !== 'all' ) { - throw new Error( '`additinalLanguages` option should be an array of language codes or `all`.' ); + throw new Error( '`additionalLanguages` option should be an array of language codes or `all`.' ); } compileAllLanguages = true; From a5dcb91a057490372b26dba3e31aaf10acad5438 Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Fri, 24 Nov 2017 14:02:55 +0100 Subject: [PATCH 25/33] Fixed docs. [skip ci] --- packages/ckeditor5-dev-webpack-plugin/lib/index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/index.js b/packages/ckeditor5-dev-webpack-plugin/lib/index.js index e255f288e..ba0be02f4 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/index.js @@ -18,13 +18,14 @@ const ckeditor5EnvUtils = require( './ckeditor5-env-utils' ); * * One entry point (or to be precise one output JS file): * - `additionalLanguages` not set -> build optimized version - * - `additionalLanguages` set –> `language` will be built into the main bundle (e.g. `ckeditor.js`). - * Translation files will be emitted in the `outputDirectory` or 'lang' directory. + * - `additionalLanguages` set –> `language` will be built into the main bundle (e.g. `ckeditor.js`) * - * Multiple output JS files + * Multiple output JS files: * - `additionalLanguages` not set -> build optimized version * - `additionalLanguages` set –> emit all translation files separately and warn user, * that he needs to load at least one translation file manually to get editor working + * + * Translation files will be emitted in the `outputDirectory` or `'lang'` directory if `outputDirectory` is not set. */ module.exports = class CKEditorWebpackPlugin { /** From 24efa6f29fbb9e06528362d8fdf2459e107ca707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Mon, 27 Nov 2017 12:26:58 +0100 Subject: [PATCH 26/33] Small change to WebPack plugin description. --- .../ckeditor5-dev-webpack-plugin/lib/index.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/index.js b/packages/ckeditor5-dev-webpack-plugin/lib/index.js index ba0be02f4..9d5f1a8b6 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/index.js @@ -14,16 +14,13 @@ const ckeditor5EnvUtils = require( './ckeditor5-env-utils' ); /** * CKEditorWebpackPlugin, for now, implements only the Translation Service (@ckeditor/ckeditor5#624, @ckeditor/ckeditor5#387). * - * Workflow: + * When one entry point (or to be precise one output JS file) is defined, language specified in `language` option will be + * statically added to the bundle. When `additionalLanguages` option is set, languages specified there will be stored + * in separate files. * - * One entry point (or to be precise one output JS file): - * - `additionalLanguages` not set -> build optimized version - * - `additionalLanguages` set –> `language` will be built into the main bundle (e.g. `ckeditor.js`) - * - * Multiple output JS files: - * - `additionalLanguages` not set -> build optimized version - * - `additionalLanguages` set –> emit all translation files separately and warn user, - * that he needs to load at least one translation file manually to get editor working + * When multiple outputs are defined, all languages (from both `language` and `additionalLanguages` options) will be + * stored in separate files. In that situation user will be warned that he needs to load at least one translation file + * manually to get editor working. * * Translation files will be emitted in the `outputDirectory` or `'lang'` directory if `outputDirectory` is not set. */ From 8a5fdcfee4aa8278ed1d84e92d1c7649538a7330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Mon, 27 Nov 2017 14:26:06 +0100 Subject: [PATCH 27/33] Small doc fix in MultipleLanguageTranslationService. --- .../lib/translations/multiplelanguagetranslationservice.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js index 78e33f82a..74b768275 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js @@ -24,8 +24,8 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * @param {Object} options * @param {Boolean} [options.compileAllLanguages=false] Flag indicates whether the languages are specified * or should be found at runtime. - * @param {Array.} options.additionalLanguages Additional languages. Build is optimized for this option is not set. - * When option is set to 'all' then script will be looking for all languages and according translations during the compilation. + * @param {Array.} options.additionalLanguages Additional languages which files will be emitted. + * When option is set to 'all', all languages found during the compilation will be added. */ constructor( language, { additionalLanguages, compileAllLanguages = false } = {} ) { super(); From 5761172cc010e09eb3b8c6b1e3277e412434cf93 Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Mon, 27 Nov 2017 14:44:18 +0100 Subject: [PATCH 28/33] Fixed errors and warnings in CKEditorWebpackPlugin`. --- .../ckeditor5-dev-webpack-plugin/lib/index.js | 10 +++- .../tests/index.js | 59 ++++++++++++++----- 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/index.js b/packages/ckeditor5-dev-webpack-plugin/lib/index.js index 9d5f1a8b6..bf6ba3a36 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/index.js @@ -42,6 +42,10 @@ module.exports = class CKEditorWebpackPlugin { apply( compiler ) { if ( !this.options.language ) { + console.warn( chalk.yellow( + 'Warning: `language` option is required for CKEditorWebpackPlugin plugin.' + ) ); + return; } @@ -52,7 +56,7 @@ module.exports = class CKEditorWebpackPlugin { if ( typeof additionalLanguages == 'string' ) { if ( additionalLanguages !== 'all' ) { - throw new Error( '`additionalLanguages` option should be an array of language codes or `all`.' ); + throw new Error( 'Error: `additionalLanguages` option should be an array of language codes or `all`.' ); } compileAllLanguages = true; @@ -61,8 +65,8 @@ module.exports = class CKEditorWebpackPlugin { if ( !additionalLanguages ) { if ( this.options.outputDirectory ) { - console.warn( chalk.red( - '`outputDirectory` option does not work for one language because zero files will be emitted. It will be ignored.' + console.warn( chalk.yellow( + 'Warning: `outputDirectory` option does not work for one language. It will be ignored.' ) ); } diff --git a/packages/ckeditor5-dev-webpack-plugin/tests/index.js b/packages/ckeditor5-dev-webpack-plugin/tests/index.js index b9a4344be..77e3da61b 100644 --- a/packages/ckeditor5-dev-webpack-plugin/tests/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/tests/index.js @@ -36,6 +36,8 @@ describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { '@ckeditor/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice': stubs.SingleLanguageTranslationService, '@ckeditor/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice': stubs.MultipleLanguageTranslationService } ); + + sandbox.stub( console, 'warn' ); } ); afterEach( () => { @@ -53,12 +55,15 @@ describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { } ); describe( 'apply()', () => { - it( 'should return and do nothing if language is not specified', () => { - const options = {}; - - const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( options ); + it( 'should log a warning and do nothing if language is not specified', () => { + const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( {} ); ckeditorWebpackPlugin.apply( {} ); + sinon.assert.calledOnce( console.warn ); + expect( console.warn.getCall( 0 ).args[ 0 ] ).to.match( + /Warning: `language` option is required for CKEditorWebpackPlugin plugin\./ + ); + sinon.assert.notCalled( stubs.serveTranslations ); } ); @@ -76,30 +81,26 @@ describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { sinon.assert.calledWith( stubs.serveTranslations, compiler, options ); } ); - it( 'should serve `SingleLanguageTranslationService` if only one language is provided.', () => { + it( 'should serve `SingleLanguageTranslationService` if only one language is provided', () => { const options = { language: 'pl' }; - const compiler = {}; - const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( options ); - ckeditorWebpackPlugin.apply( compiler ); + ckeditorWebpackPlugin.apply( {} ); sinon.assert.calledOnce( stubs.SingleLanguageTranslationService ); sinon.assert.calledWithExactly( stubs.SingleLanguageTranslationService, 'pl' ); } ); - it( 'should serve `MultipleLanguageTranslationService` if more than 1 language is provided.', () => { + it( 'should serve `MultipleLanguageTranslationService` if more than 1 language is provided', () => { const options = { language: 'pl', additionalLanguages: [ 'en' ] }; - const compiler = {}; - const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( options ); - ckeditorWebpackPlugin.apply( compiler ); + ckeditorWebpackPlugin.apply( {} ); sinon.assert.calledOnce( stubs.serveTranslations ); @@ -111,16 +112,14 @@ describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { ); } ); - it( 'should serve `MultipleLanguageTranslationService` if the `additionalLanguages` is set to `all`.', () => { + it( 'should serve `MultipleLanguageTranslationService` if the `additionalLanguages` is set to `all`', () => { const options = { language: 'en', additionalLanguages: 'all' }; - const compiler = {}; - const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( options ); - ckeditorWebpackPlugin.apply( compiler ); + ckeditorWebpackPlugin.apply( {} ); sinon.assert.calledOnce( stubs.serveTranslations ); @@ -131,5 +130,33 @@ describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { { compileAllLanguages: true, additionalLanguages: [] } ); } ); + + it( 'should log a warning if `additionalLanguages` is not specified while `outputDirectory` is set', () => { + const options = { + language: 'en', + outputDirectory: 'custom-lang' + }; + + const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( options ); + ckeditorWebpackPlugin.apply( {} ); + + sinon.assert.calledOnce( console.warn ); + expect( console.warn.getCall( 0 ).args[ 0 ] ).to.match( + /Warning: `outputDirectory` option does not work for one language\. It will be ignored\./ + ); + } ); + + it( 'should throw an error when provided `additionalLanguages` is type of string, but not `all`', () => { + const options = { + language: 'en', + additionalLanguages: 'abc' + }; + + const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( options ); + + expect( () => ckeditorWebpackPlugin.apply( {} ) ).to.throw( + /Error: `additionalLanguages` option should be an array of language codes or `all`\./ + ); + } ); } ); } ); From ab9a0d564303b735048c02b1613bda75643b6944 Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Mon, 27 Nov 2017 14:48:52 +0100 Subject: [PATCH 29/33] Added API docs. [skip ci] --- .../ckeditor5-dev-utils/lib/translations/shortidgenerator.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/ckeditor5-dev-utils/lib/translations/shortidgenerator.js b/packages/ckeditor5-dev-utils/lib/translations/shortidgenerator.js index 43fbefaed..3df22be9f 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/shortidgenerator.js +++ b/packages/ckeditor5-dev-utils/lib/translations/shortidgenerator.js @@ -14,6 +14,9 @@ module.exports = class ShortIdGenerator { this._idNumber = 0; } + /** + * Generate next id from chars in [a-z] range. + */ getNextId() { let number = this._idNumber; const chars = []; From 4fca55df7bcc52ce7b6354b631632a7e2ad66ad2 Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Mon, 27 Nov 2017 15:32:46 +0100 Subject: [PATCH 30/33] Changed env utils so they don\'t change the TranslationService directly. --- .../lib/ckeditor5-env-utils.js | 52 ++++++---- .../lib/servetranslations.js | 16 ++- .../tests/ckeditor5-env-utils.js | 98 ++++++++----------- 3 files changed, 83 insertions(+), 83 deletions(-) diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/ckeditor5-env-utils.js b/packages/ckeditor5-dev-webpack-plugin/lib/ckeditor5-env-utils.js index e83fb6ee6..adbf4653d 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/ckeditor5-env-utils.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/ckeditor5-env-utils.js @@ -15,54 +15,66 @@ const CKEditor5PackageSrcFileRegExp = /[/\\]ckeditor5-[^/\\]+[/\\]src[/\\].+\.js * Easily replaceable and testable set of CKEditor5 - related methods used by CKEditorWebpackPlugin internally. */ module.exports = { - loadCoreTranslations, - maybeLoadPackage, - maybeAddLoader + getCorePackage, + getPathToPackage, + getLoaders }; /** - * Resolve path to the core's translations and load them. + * Return path to the resolved core's translations. * - * @param {TranslationService} translationService + * @param {String} cwd Current working directory. * @param {Object} resolver Webpack resolver that can resolve the resource's request. + * @returns {Promise} */ -function loadCoreTranslations( cwd, translationService, resolver ) { - resolver.resolve( cwd, cwd, '@ckeditor/ckeditor5-core/src/editor/editor.js', ( err, result ) => { - const pathToCoreTranslationPackage = result.match( CKEditor5CoreRegExp )[ 0 ]; +function getCorePackage( cwd, resolver ) { + return new Promise( res => { + resolver.resolve( cwd, cwd, '@ckeditor/ckeditor5-core/src/editor/editor.js', ( err, result ) => { + const pathToCoreTranslationPackage = result.match( CKEditor5CoreRegExp )[ 0 ]; - translationService.loadPackage( pathToCoreTranslationPackage ); + res( pathToCoreTranslationPackage ); + } ); } ); } /** - * Add package to the `TranslationService` if the resource comes from `ckeditor5-*` package. + * Return path to the package if the resource comes from `ckeditor5-*` package. * - * @param {TranslationService} translationService + * @param {String} cwd Current working directory. * @param {String} resource Absolute path to the resource. + * @returns {String|null} */ -function maybeLoadPackage( cwd, translationService, resource ) { +function getPathToPackage( cwd, resource ) { const relativePathToResource = path.relative( cwd, resource ); const match = relativePathToResource.match( CKEditor5PackageNameRegExp ); - if ( match ) { - const index = relativePathToResource.search( CKEditor5PackageNameRegExp ) + match[ 0 ].length; - const pathToPackage = relativePathToResource.slice( 0, index ); - - translationService.loadPackage( pathToPackage ); + if ( !match ) { + return null; } + + const index = relativePathToResource.search( CKEditor5PackageNameRegExp ) + match[ 0 ].length; + + return relativePathToResource.slice( 0, index ); } /** * Inject loader when the file comes from ckeditor5-* packages. * + * @param {String} cwd Current working directory. * @param {String} resource Absolute path to the resource. - * @param {Array.} loaders Array of Webpack loaders. + * @param {Array.} loaders Array of Webpack loaders. + * @returns {Array.} */ -function maybeAddLoader( cwd, resource, loaders ) { +function getLoaders( cwd, resource, loaders ) { const relativePathToResource = path.relative( cwd, resource ); if ( relativePathToResource.match( CKEditor5PackageSrcFileRegExp ) ) { - loaders.unshift( path.join( __dirname, 'translatesourceloader.js' ) ); + return [ + path.join( __dirname, 'translatesourceloader.js' ), + ...loaders + ]; } + + return loaders; } diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js index 7e0de7836..c0cd4e78e 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js @@ -36,25 +36,31 @@ module.exports = function serveTranslations( compiler, options, translationServi throw new Error( chalk.red( error ) ); } - console.error( chalk.red( error ) ); + console.error( chalk.red( `Error: ${ error }` ) ); } ); translationService.on( 'warning', warning => { - console.warn( chalk.yellow( warning ) ); + console.warn( chalk.yellow( `Warning: ${ warning }` ) ); } ); // Add core translations before `translatesourceloader` starts translating. compiler.plugin( 'after-resolvers', () => { const resolver = compiler.resolvers.normal; - envUtils.loadCoreTranslations( cwd, translationService, resolver ); + envUtils.getCorePackage( cwd, resolver ).then( corePackage => { + translationService.loadPackage( corePackage ); + } ); } ); // Load translation files and add a loader if the package match requirements. compiler.plugin( 'normal-module-factory', nmf => { nmf.plugin( 'after-resolve', ( resolveOptions, done ) => { - envUtils.maybeLoadPackage( cwd, translationService, resolveOptions.resource ); - envUtils.maybeAddLoader( cwd, resolveOptions.resource, resolveOptions.loaders ); + const pathToPackage = envUtils.getPathToPackage( cwd, resolveOptions.resource ); + resolveOptions.loaders = envUtils.getLoaders( cwd, resolveOptions.resource, resolveOptions.loaders ); + + if ( pathToPackage ) { + translationService.loadPackage( pathToPackage ); + } done( null, resolveOptions ); } ); diff --git a/packages/ckeditor5-dev-webpack-plugin/tests/ckeditor5-env-utils.js b/packages/ckeditor5-dev-webpack-plugin/tests/ckeditor5-env-utils.js index 096e7259f..0776dfe76 100644 --- a/packages/ckeditor5-dev-webpack-plugin/tests/ckeditor5-env-utils.js +++ b/packages/ckeditor5-dev-webpack-plugin/tests/ckeditor5-env-utils.js @@ -44,114 +44,96 @@ describe( 'webpack-plugin/ckeditor5-env-utils', () => { mockery.deregisterAll(); } ); - describe( 'loadCoreTranslations()', () => { - it( 'should load core translations', () => { + describe( 'getCorePackage()', () => { + it( 'should return path to the core package', () => { const resolver = { resolve: ( context, requester, request, cb ) => { cb( null, 'path/to/' + request ); } }; - const translationService = { - loadPackage: sandbox.spy() - }; - - envUtils.loadCoreTranslations( 'cwd', translationService, resolver ); - - sinon.assert.calledOnce( translationService.loadPackage ); - sinon.assert.calledWithExactly( - translationService.loadPackage, - 'path/to/@ckeditor/ckeditor5-core' - ); + return envUtils.getCorePackage( 'cwd', resolver ).then( coreTranslations => { + expect( coreTranslations ).to.equal( 'path/to/@ckeditor/ckeditor5-core' ); + } ); } ); } ); - describe( 'maybeLoadPackage()', () => { - it( 'should load package if the path match the regexp', () => { - const translationService = { - loadPackage: sandbox.spy() - }; - - envUtils.maybeLoadPackage( 'path', translationService, 'path/to/@ckeditor/ckeditor5-utils/src/util.js' ); + describe( 'getPathToPackage()', () => { + it( 'should return package if the path match the regexp', () => { + const pathToPackage = envUtils.getPathToPackage( 'path', 'path/to/@ckeditor/ckeditor5-utils/src/util.js' ); - sinon.assert.calledOnce( translationService.loadPackage ); - sinon.assert.calledWithExactly( translationService.loadPackage, 'to/@ckeditor/ckeditor5-utils/' ); + expect( pathToPackage ).to.equal( 'to/@ckeditor/ckeditor5-utils/' ); } ); - it( 'should not load package if the path do not match the regexp', () => { - const translationService = { - loadPackage: sandbox.spy() - }; - - envUtils.maybeLoadPackage( 'path', translationService, 'path/to/@ckeditor/ckeditor5/src/util.js' ); + it( 'should return null if the path does not match the regexp', () => { + const pathToPackage = envUtils.getPathToPackage( 'path', 'path/to/@ckeditor/ckeditor5/src/util.js' ); - sinon.assert.notCalled( translationService.loadPackage ); + expect( pathToPackage ).to.equal( null ); } ); it( 'should work with Windows paths', () => { useWindowsPaths(); - const translationService = { - loadPackage: sandbox.spy() - }; - - envUtils.maybeLoadPackage( 'path', translationService, 'path\\to\\@ckeditor\\ckeditor5-utils\\src\\util.js' ); + const pathToPackage = envUtils.getPathToPackage( 'path', 'path\\to\\@ckeditor\\ckeditor5-utils\\src\\util.js' ); - sinon.assert.calledOnce( translationService.loadPackage ); - sinon.assert.calledWithExactly( translationService.loadPackage, 'to\\@ckeditor\\ckeditor5-utils\\' ); + expect( pathToPackage ).to.equal( 'to\\@ckeditor\\ckeditor5-utils\\' ); } ); it( 'should work with nested ckeditor5 packages', () => { - const translationService = { - loadPackage: sandbox.spy() - }; - - envUtils.maybeLoadPackage( + const pathToPackage = envUtils.getPathToPackage( 'path/to/ckeditor5-build-classic', - translationService, 'path/to/ckeditor5-build-classic/node_modules/@ckeditor/ckeditor5-utils/src/util.js' ); - sinon.assert.calledOnce( translationService.loadPackage ); - sinon.assert.calledWithExactly( translationService.loadPackage, 'node_modules/@ckeditor/ckeditor5-utils/' ); + expect( pathToPackage ).to.equal( 'node_modules/@ckeditor/ckeditor5-utils/' ); } ); } ); - describe( 'maybeAddLoader()', () => { - it( 'should add a loader ot the resource if the resource\'s path match the RegExp on posix systems', () => { + describe( 'getLoaders()', () => { + it( 'should return an array of loaders if the resource\'s path match the RegExp on posix systems', () => { const cwd = 'path'; const resource = 'path/to/@ckeditor/ckeditor5-utils/src/util.js'; const loaders = []; - envUtils.maybeAddLoader( cwd, resource, loaders ); + const newLoaders = envUtils.getLoaders( cwd, resource, loaders ); - expect( loaders ).does.deep.equal( [ + expect( newLoaders ).to.deep.equal( [ originalPath.normalize( originalPath.join( __dirname, '../lib/translatesourceloader.js' ) ) ] ); } ); - it( 'should add a loader ot the resource if the resource\'s path match the RegExp on the Windows systems', () => { + it( 'should not change the original array of loaders', () => { + const cwd = 'path'; + const resource = 'path/to/@ckeditor/ckeditor5-utils/src/util.js'; + const loaders = []; + + envUtils.getLoaders( cwd, resource, loaders ); + + expect( loaders ).to.deep.equal( [] ); + } ); + + it( 'should add a loader to the resource if the resource\'s path match the RegExp on the Windows systems', () => { useWindowsPaths(); const cwd = 'path'; const resource = 'path\\to\\@ckeditor\\ckeditor5-utils\\src\\util.js'; const loaders = []; - envUtils.maybeAddLoader( cwd, resource, loaders ); + const newLoaders = envUtils.getLoaders( cwd, resource, loaders ); - expect( loaders ).does.deep.equal( [ + expect( newLoaders ).to.deep.equal( [ path.normalize( path.join( __dirname, '..\\lib\\translatesourceloader.js' ) ) ] ); } ); - it( 'should not add a loader ot the resource if the resource\'s path do not match the RegExp on posix systems', () => { + it( 'should not add a loader to the resource if the resource\'s path do not match the RegExp on posix systems', () => { const cwd = 'path'; const resource = 'path/to/@ckeditor/ckeditor5/src/util.js'; const loaders = []; - envUtils.maybeAddLoader( cwd, resource, loaders ); + const newLoaders = envUtils.getLoaders( cwd, resource, loaders ); - expect( loaders.length ).to.equal( 0 ); + expect( newLoaders.length ).to.equal( 0 ); } ); it( 'should work with nested ckeditor5 packages', () => { @@ -159,9 +141,9 @@ describe( 'webpack-plugin/ckeditor5-env-utils', () => { const resource = 'path/to/ckeditor5-build-classic/node_modules/@ckeditor/ckeditor5/src/util.js'; const loaders = []; - envUtils.maybeAddLoader( cwd, resource, loaders ); + const newLoaders = envUtils.getLoaders( cwd, resource, loaders ); - expect( loaders.length ).to.equal( 0 ); + expect( newLoaders.length ).to.equal( 0 ); } ); it( 'should work with nested ckeditor5 packages #2', () => { @@ -169,9 +151,9 @@ describe( 'webpack-plugin/ckeditor5-env-utils', () => { const resource = 'path/to/ckeditor5-build-classic/node_modules/@ckeditor/ckeditor5-utils/src/util.js'; const loaders = []; - envUtils.maybeAddLoader( cwd, resource, loaders ); + const newLoaders = envUtils.getLoaders( cwd, resource, loaders ); - expect( loaders ).does.deep.equal( [ + expect( newLoaders ).does.deep.equal( [ path.normalize( path.join( __dirname, '../lib/translatesourceloader.js' ) ) ] ); } ); From d035f1a226d98702fe0f52ce227c173cdbeeeea3 Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Wed, 29 Nov 2017 11:53:28 +0100 Subject: [PATCH 31/33] Provided a 'verbose' flag. --- .../multiplelanguagetranslationservice.js | 12 +++-- .../singlelanguagetranslationservice.js | 2 +- .../multiplelanguagetranslationservice.js | 50 +++++++++++++++---- .../singlelanguagetranslationservice.js | 6 +-- .../ckeditor5-dev-webpack-plugin/lib/index.js | 3 +- .../lib/servetranslations.js | 7 ++- 6 files changed, 58 insertions(+), 22 deletions(-) diff --git a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js index 74b768275..d54805eab 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js @@ -73,7 +73,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * Load package and tries to get PO files from the package if it's unknown. * If the `compileAllLanguages` flag is set to true, language's set will be expanded by the found languages. * - * @fires error + * @fires warning * @param {String} pathToPackage Path to the package containing translations. */ loadPackage( pathToPackage ) { @@ -92,7 +92,10 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { 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.` ); + this.emit( + 'warning', + `Translation directory (${ pathToTranslationDirectory }) should contain only translation files.` + ); continue; } @@ -117,8 +120,9 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { /** * Return an array of assets based on the stored dictionaries. * If there is one `compilationAssets`, merge main translation with that asset and join with other assets built outside. - * Otherwise fire an error and retuen an array of assets built outside of the `compilationAssets`. + * Otherwise fire an warning and return an array of assets built outside of the `compilationAssets`. * + * @fires warning * @fires error * @param {Object} options * @param {String} [options.outputDirectory] Output directory for the translation files relative to the output. @@ -193,7 +197,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { const translatedString = langDictionary[ originalString ]; if ( !translatedString ) { - this.emit( 'error', `Missing translation for '${ originalString }' for ${ lang } language.` ); + this.emit( 'warning', `Missing translation for '${ originalString }' for '${ lang }' language.` ); } translatedStrings[ id ] = translatedString || originalString; diff --git a/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js index 8008d956c..8ab8d2aeb 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js @@ -88,7 +88,7 @@ module.exports = class SingleLanguageTranslationService extends EventEmitter { _translateString( originalString, sourceFile ) { if ( !this._dictionary[ originalString ] ) { - this.emit( 'error', `Missing translation for '${ originalString }' for ${ this._language } language in ${ sourceFile }.` ); + this.emit( 'warning', `Missing translation for '${ originalString }' for '${ this._language }' language in ${ sourceFile }.` ); return originalString; } diff --git a/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js index 73ab2c026..3ac6a2ef9 100644 --- a/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js @@ -249,10 +249,8 @@ describe( 'translations', () => { } } ); - sinon.assert.calledThrice( spy ); + sinon.assert.calledOnce( spy ); sinon.assert.calledWithExactly( spy, 'No translation found for xxx language.' ); - sinon.assert.calledWithExactly( spy, 'Missing translation for \'Cancel\' for xxx language.' ); - sinon.assert.calledWithExactly( spy, 'Missing translation for \'Save\' for xxx language.' ); } ); it( 'should feed missing translation with the translation key if the translated string is missing', () => { @@ -291,11 +289,14 @@ describe( 'translations', () => { ] ); } ); - it( 'should emit an error if the main translation is missing', () => { - const translationService = new MultipleLanguageTranslationService( 'xxx', { additionalLanguages: [ 'pl' ] } ); - const spy = sandbox.spy(); + it( 'should emit an error if the translations for the main language are missing', () => { + const translationService = new MultipleLanguageTranslationService( 'xxx', { + additionalLanguages: [ 'pl' ] + } ); - translationService.on( 'error', spy ); + const errorSpy = sandbox.spy(); + + translationService.on( 'error', errorSpy ); translationService._translationIdsDictionary = { Cancel: 'a', @@ -315,10 +316,37 @@ describe( 'translations', () => { } } ); - sinon.assert.calledThrice( spy ); - sinon.assert.calledWithExactly( spy, 'No translation found for xxx language.' ); - sinon.assert.calledWithExactly( spy, 'Missing translation for \'Cancel\' for xxx language.' ); - sinon.assert.calledWithExactly( spy, 'Missing translation for \'Save\' for xxx language.' ); + sinon.assert.calledOnce( errorSpy ); + sinon.assert.calledWithExactly( errorSpy, 'No translation found for xxx language.' ); + } ); + + it( 'should emit an warning if the translation is missing', () => { + const translationService = new MultipleLanguageTranslationService( 'pl', { + additionalLanguages: [] + } ); + const warningSpy = sandbox.spy(); + + translationService.on( 'warning', warningSpy ); + + translationService._translationIdsDictionary = { + Cancel: 'a', + Save: 'b' + }; + + translationService._dictionary = { + pl: { + Cancel: 'Anuluj' + } + }; + + translationService.getAssets( { + compilationAssets: { + 'ckeditor.js': { source: () => 'source' } + } + } ); + + sinon.assert.calledOnce( warningSpy ); + sinon.assert.calledWithExactly( warningSpy, 'Missing translation for \'Save\' for \'pl\' language.' ); } ); it( 'should bound to assets only used translations', () => { diff --git a/packages/ckeditor5-dev-utils/tests/translations/singlelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/tests/translations/singlelanguagetranslationservice.js index 946dbc6ab..73dcfe86f 100644 --- a/packages/ckeditor5-dev-utils/tests/translations/singlelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/tests/translations/singlelanguagetranslationservice.js @@ -105,18 +105,18 @@ describe( 'translations', () => { expect( result ).to.equal( 'translate( \'Cancel\' )' ); } ); - it( 'should emit an error and keep original string if the translation is missing', () => { + it( 'should emit a warning and keep original string if the translation is missing', () => { const translationService = new SingleLanguageTranslationService( 'pl' ); const source = 't( \'Cancel\' )'; const spy = sandbox.spy(); - translationService.on( 'error', spy ); + translationService.on( 'warning', spy ); const result = translationService.translateSource( source, 'file.js' ); expect( result ).to.equal( 't(\'Cancel\');' ); sinon.assert.calledOnce( spy ); - sinon.assert.calledWithExactly( spy, 'Missing translation for \'Cancel\' for pl language in file.js.' ); + sinon.assert.calledWithExactly( spy, 'Missing translation for \'Cancel\' for \'pl\' language in file.js.' ); } ); it( 'should throw an error when the t is called with the variable', () => { diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/index.js b/packages/ckeditor5-dev-webpack-plugin/lib/index.js index bf6ba3a36..ef35e47d7 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/index.js @@ -35,6 +35,7 @@ module.exports = class CKEditorWebpackPlugin { * should be relative to the webpack context. * @param {Boolean} [options.throwErrorOnMissingTranslation] Option that make the plugin throw when the translation is missing. * By default original (english translation keys) are used when the target translation is missing. + * @param {Boolean} [options.verbose] Option that make this plugin log everything into the console. */ constructor( options = {} ) { this.options = options; @@ -64,7 +65,7 @@ module.exports = class CKEditorWebpackPlugin { } if ( !additionalLanguages ) { - if ( this.options.outputDirectory ) { + if ( this.options.outputDirectory && this.options.verbose ) { console.warn( chalk.yellow( 'Warning: `outputDirectory` option does not work for one language. It will be ignored.' ) ); diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js index c0cd4e78e..5ba16279a 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js @@ -20,6 +20,7 @@ const chalk = require( 'chalk' ); * should be relative to the webpack context. * @param {Boolean} [options.throwErrorOnMissingTranslation] Option that make this function throw when the translation is missing. * By default original (english translation keys) are used when the target translation is missing. + * @param {Boolean} [options.verbose] Option that make this function log everything into the console. * @param {TranslationService} translationService Translation service that will load PO files, replace translation keys and generate assets. * @param {Object} envUtils Environment utils internally called within the `serveTranslations()`, that make `serveTranslations()` * ckeditor5 - independent without hard-to-test logic. @@ -30,7 +31,7 @@ module.exports = function serveTranslations( compiler, options, translationServi // Provides translateSource method for the `translatesourceloader` loader. compiler.options.translateSource = ( source, sourceFile ) => translationService.translateSource( source, sourceFile ); - // Watch for errors during translation process. + // Watch for warnings and errors during translation process. translationService.on( 'error', error => { if ( options.throwErrorOnMissingTranslation ) { throw new Error( chalk.red( error ) ); @@ -40,7 +41,9 @@ module.exports = function serveTranslations( compiler, options, translationServi } ); translationService.on( 'warning', warning => { - console.warn( chalk.yellow( `Warning: ${ warning }` ) ); + if ( options.verbose ) { + console.warn( chalk.yellow( `Warning: ${ warning }` ) ); + } } ); // Add core translations before `translatesourceloader` starts translating. From ab84ca0c995e88c47c40bf5b000319b666c660c2 Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Wed, 29 Nov 2017 12:31:08 +0100 Subject: [PATCH 32/33] Changed 'throwOnMissingTranslation' to the 'strict' option. --- packages/ckeditor5-dev-webpack-plugin/lib/index.js | 5 ++--- .../ckeditor5-dev-webpack-plugin/lib/servetranslations.js | 5 ++--- packages/ckeditor5-dev-webpack-plugin/tests/index.js | 3 ++- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/index.js b/packages/ckeditor5-dev-webpack-plugin/lib/index.js index ef35e47d7..1e1736671 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/index.js @@ -33,9 +33,8 @@ module.exports = class CKEditorWebpackPlugin { * the compilation. * @param {String} [options.outputDirectory='lang'] Output directory for the emitted translation files, * should be relative to the webpack context. - * @param {Boolean} [options.throwErrorOnMissingTranslation] Option that make the plugin throw when the translation is missing. - * By default original (english translation keys) are used when the target translation is missing. - * @param {Boolean} [options.verbose] Option that make this plugin log everything into the console. + * @param {Boolean} [options.strict] Option that make the plugin throw when the error is found during the compilation. + * @param {Boolean} [options.verbose] Option that make this plugin log all warnings into the console. */ constructor( options = {} ) { this.options = options; diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js index 5ba16279a..d466c82d7 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js @@ -18,8 +18,7 @@ const chalk = require( 'chalk' ); * @param {Array.} options.languages Target languages. * @param {String} [options.outputDirectory='lang'] Output directory for the emitted translation files, * should be relative to the webpack context. - * @param {Boolean} [options.throwErrorOnMissingTranslation] Option that make this function throw when the translation is missing. - * By default original (english translation keys) are used when the target translation is missing. + * @param {Boolean} [options.strict] Option that make this function throw when the error is found during the compilation. * @param {Boolean} [options.verbose] Option that make this function log everything into the console. * @param {TranslationService} translationService Translation service that will load PO files, replace translation keys and generate assets. * @param {Object} envUtils Environment utils internally called within the `serveTranslations()`, that make `serveTranslations()` @@ -33,7 +32,7 @@ module.exports = function serveTranslations( compiler, options, translationServi // Watch for warnings and errors during translation process. translationService.on( 'error', error => { - if ( options.throwErrorOnMissingTranslation ) { + if ( options.strict ) { throw new Error( chalk.red( error ) ); } diff --git a/packages/ckeditor5-dev-webpack-plugin/tests/index.js b/packages/ckeditor5-dev-webpack-plugin/tests/index.js index 77e3da61b..75d1afe60 100644 --- a/packages/ckeditor5-dev-webpack-plugin/tests/index.js +++ b/packages/ckeditor5-dev-webpack-plugin/tests/index.js @@ -134,7 +134,8 @@ describe( 'webpack-plugin/CKEditorWebpackPlugin', () => { it( 'should log a warning if `additionalLanguages` is not specified while `outputDirectory` is set', () => { const options = { language: 'en', - outputDirectory: 'custom-lang' + outputDirectory: 'custom-lang', + verbose: true }; const ckeditorWebpackPlugin = new CKEditorWebpackPlugin( options ); From e36663c67d1c8bcb782f0e8a4477d2595139d0e2 Mon Sep 17 00:00:00 2001 From: Maciej Bukowski Date: Wed, 29 Nov 2017 14:50:01 +0100 Subject: [PATCH 33/33] Improved API docs. --- .../multiplelanguagetranslationservice.js | 80 ++++++++++++++++--- .../singlelanguagetranslationservice.js | 52 ++++++++++-- 2 files changed, 114 insertions(+), 18 deletions(-) diff --git a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js index d54805eab..6324506b3 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js @@ -30,22 +30,55 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { constructor( language, { additionalLanguages, compileAllLanguages = false } = {} ) { super(); + /** + * Main language that should be built in to the bundle. + * + * @private + */ this._mainLanguage = language; + /** + * Set of languages that will be used by translator. This set might be expanded by found languages, + * if `compileAllLanguages` is turned on. + * + * @private + */ this._languages = new Set( [ language, ...additionalLanguages ] ); + /** + * Option indicates whether the languages are specified or should be found at runtime. + * + * @private + */ this._compileAllLanguages = compileAllLanguages; - // Set of handled packages that speed things up. + /** + * Set of handled packages that speeds up the translation process. + * + * @private + */ this._handledPackages = new Set(); - // language -> translationKey -> targetTranslation dictionary. + /** + * language -> translationKey -> targetTranslation dictionary. + * + * @private + */ this._dictionary = {}; - // translationKey -> id dictionary gathered from files parsed by loader. - // @type {Object.} + /** + * translationKey -> id dictionary gathered from files parsed by loader. + * + * @private + * @type {Object.} + */ this._translationIdsDictionary = {}; + /** + * Id generator that's used to replace translation strings with short ids and generate translation files. + * + * @private + */ this._idGenerator = new ShortIdGenerator(); } @@ -161,7 +194,13 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { ]; } - // Return assets for the given directory and languages. + /** + * Return assets for the given directory and languages. + * + * @private + * @param outputDirectory Output directory for assets. + * @param {Iterable.} languages Languages for assets. + */ _getTranslationAssets( outputDirectory, languages ) { return Array.from( languages ).map( language => { const translatedStrings = this._getIdToTranslatedStringDictionary( language ); @@ -178,8 +217,14 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { } ); } - // Walk through the `translationIdsDictionary` and find corresponding strings in the target language's dictionary. - // Use original strings if translated ones are missing. + /** + * Walk through the `translationIdsDictionary` and find corresponding strings in the target language's dictionary. + * Use original strings if translated ones are missing. + * + * @private + * @param {String} lang Target language. + * @returns {Object.} + */ _getIdToTranslatedStringDictionary( lang ) { let langDictionary = this._dictionary[ lang ]; @@ -206,7 +251,13 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { return translatedStrings; } - // Load translations from the PO files. + /** + * Load translations from the PO files. + * + * @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; @@ -226,7 +277,13 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { } } - // Translate all t() call found in source text to the target language. + /** + * Return an id for the original string. If it's stored in the `_translationIdsDictionary` return it instead of generating new one. + * + * @private + * @param {String} originalString + * @returns {String} + */ _getId( originalString ) { let id = this._translationIdsDictionary[ originalString ]; @@ -239,9 +296,12 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { } /** - * Make this fn overridable, so the class might be used in other environments than CKE5. + * Return path to the translation directory depending on the path to package. + * This method is protected to enable this class usage in other environments than CKE5. * * @protected + * @param {String} pathToPackage + * @returns {String} */ _getPathToTranslationDirectory( pathToPackage ) { return path.join( pathToPackage, 'lang', 'translations' ); diff --git a/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js index 8ab8d2aeb..2555cce61 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/singlelanguagetranslationservice.js @@ -21,10 +21,25 @@ module.exports = class SingleLanguageTranslationService extends EventEmitter { constructor( language ) { super(); + /** + * Main language that should be built in to the bundle. + * + * @private + */ this._language = language; - this._packagePaths = new Set(); - + /** + * Set of handled packages that speeds up the translation process. + * + * @private + */ + this._handledPackages = new Set(); + + /** + * translationKey -> targetTranslation dictionary. + * + * @private + */ this._dictionary = {}; } @@ -32,6 +47,7 @@ module.exports = class SingleLanguageTranslationService extends EventEmitter { * Translate file's source and replace `t()` call strings with translated strings. * * @fires error + * @fires warning * @param {String} source Source of the file. * @param {String} fileName File name. * @returns {String} @@ -53,25 +69,33 @@ module.exports = class SingleLanguageTranslationService extends EventEmitter { * @param {String} pathToPackage Path to the package containing translations. */ loadPackage( pathToPackage ) { - if ( this._packagePaths.has( pathToPackage ) ) { + if ( this._handledPackages.has( pathToPackage ) ) { return; } - this._packagePaths.add( pathToPackage ); + this._handledPackages.add( pathToPackage ); - const pathToPoFile = this._getPathToPoFile( pathToPackage, this._language ); + const pathToTranslationDirectory = this._getPathToTranslationDirectory( pathToPackage, this._language ); + const pathToPoFile = pathToTranslationDirectory + path.sep + this._language + '.po'; this._loadPoFile( pathToPoFile ); } /** * That class doesn't generate any asset. + * + * @returns {Array} */ getAssets() { return []; } - // Load translations from the PO file. + /** + * Load translations from the PO file. + * + * @private + * @param {String} pathToPoFile Path to the target PO file. + */ _loadPoFile( pathToPoFile ) { if ( !fs.existsSync( pathToPoFile ) ) { return; @@ -86,6 +110,13 @@ module.exports = class SingleLanguageTranslationService extends EventEmitter { } } + /** + * Translate original string for the target language. + * + * @private + * @param {String} originalString + * @param {String} sourceFile Path to the original string's file. + */ _translateString( originalString, sourceFile ) { if ( !this._dictionary[ originalString ] ) { this.emit( 'warning', `Missing translation for '${ originalString }' for '${ this._language }' language in ${ sourceFile }.` ); @@ -97,9 +128,14 @@ module.exports = class SingleLanguageTranslationService extends EventEmitter { } /** + * Return path to the translation directory depending on the path to package. + * This method is protected to enable this class usage in other environments than CKE5. + * * @protected + * @param {String} pathToPackage + * @returns {String} */ - _getPathToPoFile( pathToPackage, languageCode ) { - return path.join( pathToPackage, 'lang', 'translations', languageCode + '.po' ); + _getPathToTranslationDirectory( pathToPackage ) { + return path.join( pathToPackage, 'lang', 'translations' ); } };