Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ nbproject/
############

build
module-i18n.php

############
## Vendor
Expand Down
12 changes: 12 additions & 0 deletions admin/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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;
}
11 changes: 11 additions & 0 deletions bin/plugin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
33 changes: 14 additions & 19 deletions bin/plugin/commands/changelog.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand All @@ -33,7 +45,7 @@ const config = require( '../config' );
* @property {string=} token Optional personal access token.
*/

const options = [
exports.options = [
{
argname: '-m, --milestone <milestone>',
description: 'Milestone',
Expand All @@ -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.
Expand Down
166 changes: 166 additions & 0 deletions bin/plugin/commands/translations.js
Original file line number Diff line number Diff line change
@@ -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 = `<?php
/* THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY. */
$generated_i18n_strings = array(
`;
const FILE_FOOTER = `
);
/* THIS IS THE END OF THE GENERATED FILE */
`;

/**
* @typedef WPTranslationsCommandOptions
*
* @property {string=} directory Optional directory, default is the root `/modules` directory.
* @property {string=} output Optional output PHP file, default is the root `/module-i18n.php` file.
*/

/**
* @typedef WPTranslationsSettings
*
* @property {string} textDomain Plugin text domain.
* @property {string} directory Modules directory.
* @property {string} output Output PHP file.
*/

/**
* @typedef WPTranslationEntry
*
* @property {string} text String to translate.
* @property {string} context Context for translators.
*/

exports.options = [
{
argname: '-d, --directory <directory>',
description: 'Modules directory',
},
{
argname: '-d, --output <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]*<?php)?[ \t/*#@]*(${ Object.keys( headers ).join(
'|'
) }):(.*)$`,
'gmi'
);
let match = regex.exec( fileContent );
while ( match ) {
const text = match[ 2 ].trim();
const context = headers[ match[ 1 ] ];
if ( text && context ) {
translationEntries.push( {
text,
context,
} );
}
match = regex.exec( fileContent );
}

return translationEntries;
} )
.filter( ( translations ) => !! 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`
)
);
}
1 change: 1 addition & 0 deletions bin/plugin/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
const config = {
githubRepositoryOwner: 'WordPress',
githubRepositoryName: 'performance',
textDomain: 'performance-lab',
};

module.exports = config;
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@
"@wordpress/scripts": "^19.0",
"chalk": "4.1.1",
"commander": "4.1.0",
"fast-glob": "^3.2.7",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@felixarntz, we also need to update the package-lock.json to make sure everybody uses the same version of this dependency.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may have been doing something wrong, but when I tried it, the package-lock.json was unchanged. It looks like the same version of this dependency was already previously required via another dependency.

"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",
Expand Down