diff --git a/.gitignore b/.gitignore index a54ea55a46..b5f606531a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ nbproject/ ############ build +module-i18n.php ############ ## Vendor diff --git a/admin/load.php b/admin/load.php index 9863027bc3..2789bd53d9 100644 --- a/admin/load.php +++ b/admin/load.php @@ -262,6 +262,7 @@ function( $a, $b ) { * Parses the module main file to get the module's metadata. * * This is similar to how plugin data is parsed in the WordPress core function `get_plugin_data()`. + * The user-facing strings will be translated. * * @since 1.0.0 * @@ -291,5 +292,16 @@ function perflab_get_module_data( $module_file ) { $module_data['experimental'] = false; } + // Translate fields using low-level function since they come from PHP comments, including the necessary context for + // `_x()`. This must match how these are translated in the generated `/module-i18n.php` file. + $translatable_fields = array( + 'name' => 'module name', + 'description' => 'module description', + ); + foreach ( $translatable_fields as $field => $context ) { + // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralContext,WordPress.WP.I18n.NonSingularStringLiteralText + $module_data[ $field ] = translate_with_gettext_context( $module_data[ $field ], $context, 'performance-lab' ); + } + return $module_data; } diff --git a/bin/plugin/cli.js b/bin/plugin/cli.js index f92873de0d..11fbccdefd 100755 --- a/bin/plugin/cli.js +++ b/bin/plugin/cli.js @@ -30,10 +30,21 @@ const { handler: changelogHandler, options: changelogOptions, } = require( './commands/changelog' ); +const { + handler: translationsHandler, + options: translationsOptions, +} = require( './commands/translations' ); withOptions( program.command( 'release-plugin-changelog' ), changelogOptions ) .alias( 'changelog' ) .description( 'Generates a changelog from merged pull requests' ) .action( catchException( changelogHandler ) ); +withOptions( program.command( 'module-translations' ), translationsOptions ) + .alias( 'translations' ) + .description( + 'Generates a PHP file from module header translation strings' + ) + .action( catchException( translationsHandler ) ); + program.parse( process.argv ); diff --git a/bin/plugin/commands/changelog.js b/bin/plugin/commands/changelog.js index 237fb43331..316381d392 100644 --- a/bin/plugin/commands/changelog.js +++ b/bin/plugin/commands/changelog.js @@ -14,6 +14,18 @@ const { } = require( '../lib/milestone' ); const config = require( '../config' ); +const MISSING_TYPE = 'MISSING_TYPE'; +const MISSING_FOCUS = 'MISSING_FOCUS'; +const TYPE_PREFIX = '[Type] '; +const FOCUS_PREFIX = '[Focus] '; +const INFRASTRUCTURE_LABEL = 'Infrastructure'; +const PRIMARY_TYPE_LABELS = { + '[Type] Feature': 'Features', + '[Type] Enhancement': 'Enhancements', + '[Type] Bug': 'Bug Fixes', +}; +const PRIMARY_TYPE_ORDER = Object.values( PRIMARY_TYPE_LABELS ); + /** @typedef {import('@octokit/rest')} GitHub */ /** @typedef {import('@octokit/rest').IssuesListForRepoResponseItem} IssuesListForRepoResponseItem */ @@ -33,7 +45,7 @@ const config = require( '../config' ); * @property {string=} token Optional personal access token. */ -const options = [ +exports.options = [ { argname: '-m, --milestone ', description: 'Milestone', @@ -49,32 +61,15 @@ const options = [ * * @param {WPChangelogCommandOptions} opt */ -async function handler( opt ) { +exports.handler = async ( opt ) => { await createChangelog( { owner: config.githubRepositoryOwner, repo: config.githubRepositoryName, milestone: opt.milestone, token: opt.token, } ); -} - -module.exports = { - options, - handler, }; -const MISSING_TYPE = 'MISSING_TYPE'; -const MISSING_FOCUS = 'MISSING_FOCUS'; -const TYPE_PREFIX = '[Type] '; -const FOCUS_PREFIX = '[Focus] '; -const INFRASTRUCTURE_LABEL = 'Infrastructure'; -const PRIMARY_TYPE_LABELS = { - '[Type] Feature': 'Features', - '[Type] Enhancement': 'Enhancements', - '[Type] Bug': 'Bug Fixes', -}; -const PRIMARY_TYPE_ORDER = Object.values( PRIMARY_TYPE_LABELS ); - /** * Returns a promise resolving to an array of pull requests associated with the * changelog settings object. diff --git a/bin/plugin/commands/translations.js b/bin/plugin/commands/translations.js new file mode 100644 index 0000000000..f9afa1bb6d --- /dev/null +++ b/bin/plugin/commands/translations.js @@ -0,0 +1,166 @@ +/** + * External dependencies + */ +const path = require( 'path' ); +const glob = require( 'fast-glob' ); +const fs = require( 'fs' ); +const { EOL } = require( 'os' ); + +/** + * Internal dependencies + */ +const { log, formats } = require( '../lib/logger' ); +const config = require( '../config' ); + +const TAB = '\t'; +const NEWLINE = EOL; +const FILE_HEADER = `', + description: 'Modules directory', + }, + { + argname: '-d, --output ', + description: 'Output file', + }, +]; + +/** + * Command that generates a PHP file from module header translation strings. + * + * @param {WPTranslationsCommandOptions} opt + */ +exports.handler = async ( opt ) => { + await createTranslations( { + textDomain: config.textDomain, + directory: opt.directory || 'modules', + output: opt.output || 'module-i18n.php', + } ); +}; + +/** + * Parses module header translation strings. + * + * @param {WPTranslationsSettings} settings Translations settings. + * + * @return {[]WPTranslationEntry} List of translation entries. + */ +async function getTranslations( settings ) { + const moduleFilePattern = path.join( settings.directory, '*/load.php' ); + const moduleFiles = await glob( path.resolve( '.', moduleFilePattern ) ); + + const moduleTranslations = moduleFiles + .map( ( moduleFile ) => { + // Map of module header => translator context. + const headers = { + 'Module Name': 'module name', + Description: 'module description', + }; + const translationEntries = []; + + const fileContent = fs.readFileSync( moduleFile, 'utf8' ); + const regex = new RegExp( + `^(?:[ \t]* !! translations.length ); + + return moduleTranslations.flat(); +} + +/** + * Parses module header translation strings. + * + * @param {[]WPTranslationEntry} translations List of translation entries. + * @param {WPTranslationsSettings} settings Translations settings. + */ +function createTranslationsPHPFile( translations, settings ) { + const output = translations.map( ( translation ) => { + // Escape single quotes. + return `${ TAB }_x( '${ translation.text.replace( /'/g, "\\'" ) }', '${ + translation.context + }', '${ settings.textDomain }' ),`; + } ); + + const fileOutput = `${ FILE_HEADER }${ output.join( + NEWLINE + ) }${ FILE_FOOTER }`; + fs.writeFileSync( path.join( '.', settings.output ), fileOutput ); +} + +/** + * Parses module header translation strings and generates a PHP file with them. + * + * @param {WPTranslationsSettings} settings Translations settings. + */ +async function createTranslations( settings ) { + log( + formats.title( + `\n💃Preparing module translations for "${ settings.directory }" in "${ settings.output }"\n\n` + ) + ); + + try { + const translations = await getTranslations( settings ); + createTranslationsPHPFile( translations, settings ); + } catch ( error ) { + if ( error instanceof Error ) { + log( formats.error( error.stack ) ); + return; + } + } + + log( + formats.success( + `\n💃Module translations successfully set in "${ settings.output }"\n\n` + ) + ); +} diff --git a/bin/plugin/config.js b/bin/plugin/config.js index 00074e6c05..076169af89 100644 --- a/bin/plugin/config.js +++ b/bin/plugin/config.js @@ -11,6 +11,7 @@ const config = { githubRepositoryOwner: 'WordPress', githubRepositoryName: 'performance', + textDomain: 'performance-lab', }; module.exports = config; diff --git a/package.json b/package.json index e3b75dd174..25fb0fce22 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,12 @@ "@wordpress/scripts": "^19.0", "chalk": "4.1.1", "commander": "4.1.0", + "fast-glob": "^3.2.7", "lodash": "4.17.21" }, "scripts": { "changelog": "./bin/plugin/cli.js changelog", + "translations": "./bin/plugin/cli.js translations", "format-js": "wp-scripts format ./bin", "lint-js": "wp-scripts lint-js ./bin", "format-php": "wp-env run composer run-script format",