-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1025 from ckeditor/ci/3833
Feature (translations): Introduced `moveTranslations()` function to move requested translations between packages. It removes contexts and translated messages from language files ("*.po" files) from the source package and adds (or overwrites) them in the destination package.
- Loading branch information
Showing
16 changed files
with
1,001 additions
and
105 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
121 changes: 121 additions & 0 deletions
121
packages/ckeditor5-dev-translations/lib/movetranslations.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
/** | ||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/ | ||
|
||
import fs from 'fs-extra'; | ||
import { logger } from '@ckeditor/ckeditor5-dev-utils'; | ||
import getPackageContext from './utils/getpackagecontext.js'; | ||
import moveTranslationsBetweenPackages from './utils/movetranslationsbetweenpackages.js'; | ||
|
||
/** | ||
* Moves the requested translations (context and messages) between packages by performing the following steps: | ||
* * Detect if translations to move are not duplicated. | ||
* * Detect if both source and destination packages exist. | ||
* * Detect if translation context to move exists in the source package. Message may not exist in the target package, | ||
* but if it does, it will be overwritten. | ||
* * If there are no validation errors, move the requested translations between packages: the context and the translation | ||
* messages for each language found in the source package. | ||
* | ||
* @param {object} options | ||
* @param {Array.<TranslationMoveEntry>} options.config Configuration that defines the messages to move. | ||
*/ | ||
export default function moveTranslations( options ) { | ||
const { config } = options; | ||
const log = logger(); | ||
|
||
log.info( '📍 Loading translations contexts...' ); | ||
const packageContexts = config.flatMap( entry => [ | ||
getPackageContext( { packagePath: entry.source } ), | ||
getPackageContext( { packagePath: entry.destination } ) | ||
] ); | ||
|
||
const errors = []; | ||
|
||
log.info( '📍 Checking provided configuration...' ); | ||
errors.push( | ||
...assertTranslationMoveEntriesUnique( { config } ), | ||
...assertPackagesExist( { config } ), | ||
...assertContextsExist( { packageContexts, config } ) | ||
); | ||
|
||
if ( errors.length ) { | ||
log.error( '🔥 The following errors have been found:' ); | ||
|
||
for ( const error of errors ) { | ||
log.error( ` - ${ error }` ); | ||
} | ||
|
||
process.exit( 1 ); | ||
} | ||
|
||
log.info( '📍 Moving translations between packages...' ); | ||
moveTranslationsBetweenPackages( { packageContexts, config } ); | ||
|
||
log.info( '✨ Done.' ); | ||
} | ||
|
||
/** | ||
* @param {object} options | ||
* @param {Array.<TranslationMoveEntry>} options.config Configuration that defines the messages to move. | ||
* @returns {Array.<string>} | ||
*/ | ||
function assertTranslationMoveEntriesUnique( { config } ) { | ||
const moveEntriesGroupedByMessageId = config.reduce( ( result, entry ) => { | ||
result[ entry.messageId ] = result[ entry.messageId ] || 0; | ||
result[ entry.messageId ]++; | ||
|
||
return result; | ||
}, {} ); | ||
|
||
return Object.keys( moveEntriesGroupedByMessageId ) | ||
.filter( messageId => moveEntriesGroupedByMessageId[ messageId ] > 1 ) | ||
.map( messageId => `Duplicated entry: the "${ messageId }" message is configured to be moved multiple times.` ); | ||
} | ||
|
||
/** | ||
* @param {object} options | ||
* @param {Array.<TranslationMoveEntry>} options.config Configuration that defines the messages to move. | ||
* @returns {Array.<string>} | ||
*/ | ||
function assertPackagesExist( { config } ) { | ||
return config | ||
.flatMap( entry => { | ||
const missingPackages = []; | ||
|
||
if ( !fs.existsSync( entry.source ) ) { | ||
missingPackages.push( entry.source ); | ||
} | ||
|
||
if ( !fs.existsSync( entry.destination ) ) { | ||
missingPackages.push( entry.destination ); | ||
} | ||
|
||
return missingPackages; | ||
} ) | ||
.map( packagePath => `Missing package: the "${ packagePath }" package does not exist.` ); | ||
} | ||
|
||
/** | ||
* @param {object} options | ||
* @param {Array.<TranslationsContext>} options.packageContexts An array of language contexts. | ||
* @param {Array.<TranslationMoveEntry>} options.config Configuration that defines the messages to move. | ||
* @returns {Array.<string>} | ||
*/ | ||
function assertContextsExist( { packageContexts, config } ) { | ||
return config | ||
.filter( entry => { | ||
const packageContext = packageContexts.find( context => context.packagePath === entry.source ); | ||
|
||
return !packageContext.contextContent[ entry.messageId ]; | ||
} ) | ||
.map( entry => `Missing context: the "${ entry.messageId }" message does not exist in "${ entry.source }" package.` ); | ||
} | ||
|
||
/** | ||
* @typedef {object} TranslationMoveEntry | ||
* | ||
* @property {string} source Relative path to the source package from which the `messageId` should be moved. | ||
* @property {string} destination Relative path to the destination package to which the `messageId` should be moved. | ||
* @property {string} messageId The message identifier to move. | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
packages/ckeditor5-dev-translations/lib/utils/getpackagecontext.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
/** | ||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/ | ||
|
||
import upath from 'upath'; | ||
import fs from 'fs-extra'; | ||
import { CONTEXT_FILE_PATH } from './constants.js'; | ||
|
||
/** | ||
* @param {object} options | ||
* @param {string} options.packagePath Path to the package containing the context. | ||
* @returns {TranslationsContext} | ||
*/ | ||
export default function getPackageContext( { packagePath } ) { | ||
const contextFilePath = upath.join( packagePath, CONTEXT_FILE_PATH ); | ||
const contextContent = fs.readJsonSync( contextFilePath, { throws: false } ) || {}; | ||
|
||
return { | ||
contextContent, | ||
contextFilePath, | ||
packagePath | ||
}; | ||
} | ||
|
||
/** | ||
* @typedef {object} TranslationsContext | ||
* | ||
* @property {string} contextFilePath | ||
* @property {object} contextContent | ||
* @property {string} packagePath | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
74 changes: 74 additions & 0 deletions
74
packages/ckeditor5-dev-translations/lib/utils/movetranslationsbetweenpackages.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
/** | ||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/ | ||
|
||
import upath from 'upath'; | ||
import fs from 'fs-extra'; | ||
import PO from 'pofile'; | ||
import { glob } from 'glob'; | ||
import { TRANSLATION_FILES_PATH } from './constants.js'; | ||
import cleanTranslationFileContent from './cleantranslationfilecontent.js'; | ||
|
||
/** | ||
* @param {object} options | ||
* @param {Array.<TranslationsContext>} options.packageContexts An array of language contexts. | ||
* @param {Array.<TranslationMoveEntry>} options.config Configuration that defines the messages to move. | ||
*/ | ||
export default function moveTranslationsBetweenPackages( { packageContexts, config } ) { | ||
// For each message to move: | ||
for ( const { source, destination, messageId } of config ) { | ||
// (1) Skip the message if its source and destination package is the same. | ||
if ( source === destination ) { | ||
continue; | ||
} | ||
|
||
// (2) Move translation context from source package to destination package. | ||
const sourcePackageContext = packageContexts.find( context => context.packagePath === source ); | ||
const destinationPackageContext = packageContexts.find( context => context.packagePath === destination ); | ||
|
||
destinationPackageContext.contextContent[ messageId ] = sourcePackageContext.contextContent[ messageId ]; | ||
delete sourcePackageContext.contextContent[ messageId ]; | ||
|
||
// (3) Prepare the list of paths to translation files ("*.po" files) in source and destination packages. | ||
// The source package defines the list of files for both packages. | ||
const translationFilesPattern = upath.join( source, TRANSLATION_FILES_PATH, '*.po' ); | ||
const translationFilePaths = glob.sync( translationFilesPattern ) | ||
.map( filePath => upath.basename( filePath ) ) | ||
.map( fileName => ( { | ||
sourceTranslationFilePath: upath.join( source, TRANSLATION_FILES_PATH, fileName ), | ||
destinationTranslationFilePath: upath.join( destination, TRANSLATION_FILES_PATH, fileName ) | ||
} ) ); | ||
|
||
// Then, for each translation file: | ||
for ( const { sourceTranslationFilePath, destinationTranslationFilePath } of translationFilePaths ) { | ||
// (3.1) Read the source translation file. | ||
const sourceTranslationFile = fs.readFileSync( sourceTranslationFilePath, 'utf-8' ); | ||
const sourceTranslations = PO.parse( sourceTranslationFile ); | ||
|
||
// (3.2) Read the destination translation file. | ||
// If the destination file does not exist, use the source file as a base and remove all translations. | ||
const destinationTranslationFile = fs.existsSync( destinationTranslationFilePath ) ? | ||
fs.readFileSync( destinationTranslationFilePath, 'utf-8' ) : | ||
null; | ||
const destinationTranslations = PO.parse( destinationTranslationFile || sourceTranslationFile ); | ||
|
||
if ( !destinationTranslationFile ) { | ||
destinationTranslations.items = []; | ||
} | ||
|
||
// (3.3) Move the translation from source file to destination file. | ||
const sourceMessage = sourceTranslations.items.find( item => item.msgid === messageId ); | ||
sourceTranslations.items = sourceTranslations.items.filter( item => item.msgid !== messageId ); | ||
|
||
destinationTranslations.items = destinationTranslations.items.filter( item => item.msgid !== messageId ); | ||
destinationTranslations.items.push( sourceMessage ); | ||
|
||
fs.outputFileSync( sourceTranslationFilePath, cleanTranslationFileContent( sourceTranslations ).toString(), 'utf-8' ); | ||
fs.outputFileSync( destinationTranslationFilePath, cleanTranslationFileContent( destinationTranslations ).toString(), 'utf-8' ); | ||
} | ||
|
||
fs.outputJsonSync( sourcePackageContext.contextFilePath, sourcePackageContext.contextContent, { spaces: '\t' } ); | ||
fs.outputJsonSync( destinationPackageContext.contextFilePath, destinationPackageContext.contextContent, { spaces: '\t' } ); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.