diff --git a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js index bc5e7d07b..6f02d390d 100644 --- a/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/lib/translations/multiplelanguagetranslationservice.js @@ -176,13 +176,13 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { } const mainAssetName = compilationAssetNames[ 0 ]; - const mainCompilationAsset = compilationAssets[ mainAssetName ]; const mainTranslationAsset = this._getTranslationAssets( outputDirectory, [ this._mainLanguage ] )[ 0 ]; const mergedCompilationAsset = { - outputBody: mainCompilationAsset.source() + '\n;' + mainTranslationAsset.outputBody, - outputPath: mainAssetName + outputBody: mainTranslationAsset.outputBody, + outputPath: mainAssetName, + shouldConcat: true }; const otherLanguages = Array.from( this._languages ) @@ -211,7 +211,16 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { const stringifiedTranslations = JSON.stringify( translatedStrings ) .replace( /"([a-z]+)":/g, '$1:' ); - const outputBody = `CKEDITOR_TRANSLATIONS.add('${ language }',${ stringifiedTranslations })`; + const outputBody = ( + // We need to ensure that the CKEDITOR_TRANSLATIONS variable exists and if it exists, we need to extend it. + // Use ES5 because this bit will not be transpiled! + '(function(d){' + + `d['${ language }']=Object.assign(` + + `d['${ language }']||{},` + + `${ stringifiedTranslations }` + + ')' + + '})(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));' + ); return { outputBody, outputPath }; } ); diff --git a/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js index 22ea81907..cbf5b9935 100644 --- a/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-utils/tests/translations/multiplelanguagetranslationservice.js @@ -217,11 +217,14 @@ describe( 'translations', () => { expect( assets ).to.deep.equal( [ { outputPath: 'ckeditor.js', - outputBody: 'source\n;CKEDITOR_TRANSLATIONS.add(\'pl\',{a:"Anuluj",b:"Zapisz"})' + outputBody: '(function(d){d[\'pl\']=Object.assign(d[\'pl\']||{},{a:"Anuluj",b:"Zapisz"})})' + + '(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));', + shouldConcat: true }, { outputPath: path.join( 'lang', 'en.js' ), - outputBody: 'CKEDITOR_TRANSLATIONS.add(\'en\',{a:"Cancel",b:"Save"})' + outputBody: '(function(d){d[\'en\']=Object.assign(d[\'en\']||{},{a:"Cancel",b:"Save"})})' + + '(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));' } ] ); } ); @@ -283,11 +286,14 @@ describe( 'translations', () => { expect( assets ).to.deep.equal( [ { outputPath: 'ckeditor.js', - outputBody: 'source\n;CKEDITOR_TRANSLATIONS.add(\'pl\',{a:"Anuluj",b:"Zapisz"})' + outputBody: '(function(d){d[\'pl\']=Object.assign(d[\'pl\']||{},{a:"Anuluj",b:"Zapisz"})})' + + '(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));', + shouldConcat: true }, { outputPath: path.join( 'lang', 'xxx.js' ), - outputBody: 'CKEDITOR_TRANSLATIONS.add(\'xxx\',{a:"Cancel",b:"Save"})' + outputBody: '(function(d){d[\'xxx\']=Object.assign(d[\'xxx\']||{},{a:"Cancel",b:"Save"})})' + + '(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));' } ] ); } ); @@ -381,7 +387,9 @@ describe( 'translations', () => { expect( assets ).to.deep.equal( [ { outputPath: 'ckeditor.js', - outputBody: 'source\n;CKEDITOR_TRANSLATIONS.add(\'pl\',{a:"Anuluj",b:"Zapisz"})' + outputBody: '(function(d){d[\'pl\']=Object.assign(d[\'pl\']||{},{a:"Anuluj",b:"Zapisz"})})' + + '(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));', + shouldConcat: true } ] ); } ); @@ -421,7 +429,8 @@ describe( 'translations', () => { expect( assets ).to.deep.equal( [ { outputPath: path.join( 'lang', 'pl.js' ), - outputBody: 'CKEDITOR_TRANSLATIONS.add(\'pl\',{a:"Anuluj",b:"Zapisz"})' + outputBody: '(function(d){d[\'pl\']=Object.assign(d[\'pl\']||{},{a:"Anuluj",b:"Zapisz"})})' + + '(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));' } ] ); } ); @@ -457,11 +466,14 @@ describe( 'translations', () => { expect( assets ).to.deep.equal( [ { outputPath: 'ckeditor.js', - outputBody: 'source\n;CKEDITOR_TRANSLATIONS.add(\'pl\',{a:"Anuluj"})' + outputBody: '(function(d){d[\'pl\']=Object.assign(d[\'pl\']||{},{a:"Anuluj"})})' + + '(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));', + shouldConcat: true }, { outputPath: path.join( 'custom-lang-path', 'en.js' ), - outputBody: 'CKEDITOR_TRANSLATIONS.add(\'en\',{a:"Cancel"})' + outputBody: '(function(d){d[\'en\']=Object.assign(d[\'en\']||{},{a:"Cancel"})})' + + '(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));' } ] ); } ); @@ -538,11 +550,14 @@ describe( 'translations', () => { expect( assets ).to.deep.equal( [ { outputPath: 'ckeditor.js', - outputBody: 'source\n;CKEDITOR_TRANSLATIONS.add(\'pl\',{a:"Zapisz"})' + outputBody: '(function(d){d[\'pl\']=Object.assign(d[\'pl\']||{},{a:"Zapisz"})})' + + '(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));', + shouldConcat: true }, { outputPath: path.join( 'lang', 'de.js' ), - outputBody: 'CKEDITOR_TRANSLATIONS.add(\'de\',{a:"Speichern"})' + outputBody: '(function(d){d[\'de\']=Object.assign(d[\'de\']||{},{a:"Speichern"})})' + + '(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));' } ] ); } ); diff --git a/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js index cb5741150..5c6611820 100644 --- a/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js +++ b/packages/ckeditor5-dev-webpack-plugin/lib/servetranslations.js @@ -9,6 +9,7 @@ const chalk = require( 'chalk' ); const rimraf = require( 'rimraf' ); const fs = require( 'fs' ); const path = require( 'path' ); +const { RawSource, ConcatSource } = require( 'webpack-sources' ); /** * Serve translations depending on the used translation service and passed options. @@ -77,20 +78,37 @@ module.exports = function serveTranslations( compiler, options, translationServi } ); // At the end of the compilation add assets generated from the PO files. - 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, - }; - } + // Use `optimize-chunk-assets` instead of `emit` to emit assets before the `webpack.BannerPlugin`. + compiler.plugin( 'compilation', compilation => { + compilation.plugin( 'optimize-chunk-assets', ( chunks, done ) => { + const generatedAssets = translationService.getAssets( { + outputDirectory: options.outputDirectory, + compilationAssets: compilation.assets + } ); + + const allFiles = chunks.reduce( ( acc, chunk ) => [ ...acc, ...chunk.files ], [] ); + + for ( const asset of generatedAssets ) { + if ( asset.shouldConcat ) { + // We need to concat sources here to support source maps for CKE5 code. + const originalAsset = compilation.assets[ asset.outputPath ]; + compilation.assets[ asset.outputPath ] = new ConcatSource( asset.outputBody, '\n', originalAsset ); + } else { + const chunkExists = allFiles.includes( asset.outputPath ); + + if ( !chunkExists ) { + // RawSource is used when corresponding chunk does not exist. + compilation.assets[ asset.outputPath ] = new RawSource( asset.outputBody ); + } else { + // String is used when corresponding chunk exists and maintain proper sourcemaps. + // Changing to RawSource would drop source maps. + compilation.assets[ asset.outputPath ] = asset.outputBody; + } + } + } - done(); + done(); + } ); } ); function emitError( error ) {