Skip to content

Commit cae6050

Browse files
authored
Merge pull request #60 from WordPress/fix/59-module-headers-translatable
Ensure that module header fields can be translated
2 parents 77d5ffd + 7aa49c7 commit cae6050

File tree

7 files changed

+207
-19
lines changed

7 files changed

+207
-19
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ nbproject/
2222
############
2323

2424
build
25+
module-i18n.php
2526

2627
############
2728
## Vendor

admin/load.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ function( $a, $b ) {
262262
* Parses the module main file to get the module's metadata.
263263
*
264264
* This is similar to how plugin data is parsed in the WordPress core function `get_plugin_data()`.
265+
* The user-facing strings will be translated.
265266
*
266267
* @since 1.0.0
267268
*
@@ -291,5 +292,16 @@ function perflab_get_module_data( $module_file ) {
291292
$module_data['experimental'] = false;
292293
}
293294

295+
// Translate fields using low-level function since they come from PHP comments, including the necessary context for
296+
// `_x()`. This must match how these are translated in the generated `/module-i18n.php` file.
297+
$translatable_fields = array(
298+
'name' => 'module name',
299+
'description' => 'module description',
300+
);
301+
foreach ( $translatable_fields as $field => $context ) {
302+
// phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralContext,WordPress.WP.I18n.NonSingularStringLiteralText
303+
$module_data[ $field ] = translate_with_gettext_context( $module_data[ $field ], $context, 'performance-lab' );
304+
}
305+
294306
return $module_data;
295307
}

bin/plugin/cli.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,21 @@ const {
3030
handler: changelogHandler,
3131
options: changelogOptions,
3232
} = require( './commands/changelog' );
33+
const {
34+
handler: translationsHandler,
35+
options: translationsOptions,
36+
} = require( './commands/translations' );
3337

3438
withOptions( program.command( 'release-plugin-changelog' ), changelogOptions )
3539
.alias( 'changelog' )
3640
.description( 'Generates a changelog from merged pull requests' )
3741
.action( catchException( changelogHandler ) );
3842

43+
withOptions( program.command( 'module-translations' ), translationsOptions )
44+
.alias( 'translations' )
45+
.description(
46+
'Generates a PHP file from module header translation strings'
47+
)
48+
.action( catchException( translationsHandler ) );
49+
3950
program.parse( process.argv );

bin/plugin/commands/changelog.js

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@ const {
1414
} = require( '../lib/milestone' );
1515
const config = require( '../config' );
1616

17+
const MISSING_TYPE = 'MISSING_TYPE';
18+
const MISSING_FOCUS = 'MISSING_FOCUS';
19+
const TYPE_PREFIX = '[Type] ';
20+
const FOCUS_PREFIX = '[Focus] ';
21+
const INFRASTRUCTURE_LABEL = 'Infrastructure';
22+
const PRIMARY_TYPE_LABELS = {
23+
'[Type] Feature': 'Features',
24+
'[Type] Enhancement': 'Enhancements',
25+
'[Type] Bug': 'Bug Fixes',
26+
};
27+
const PRIMARY_TYPE_ORDER = Object.values( PRIMARY_TYPE_LABELS );
28+
1729
/** @typedef {import('@octokit/rest')} GitHub */
1830
/** @typedef {import('@octokit/rest').IssuesListForRepoResponseItem} IssuesListForRepoResponseItem */
1931

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

36-
const options = [
48+
exports.options = [
3749
{
3850
argname: '-m, --milestone <milestone>',
3951
description: 'Milestone',
@@ -49,32 +61,15 @@ const options = [
4961
*
5062
* @param {WPChangelogCommandOptions} opt
5163
*/
52-
async function handler( opt ) {
64+
exports.handler = async ( opt ) => {
5365
await createChangelog( {
5466
owner: config.githubRepositoryOwner,
5567
repo: config.githubRepositoryName,
5668
milestone: opt.milestone,
5769
token: opt.token,
5870
} );
59-
}
60-
61-
module.exports = {
62-
options,
63-
handler,
6471
};
6572

66-
const MISSING_TYPE = 'MISSING_TYPE';
67-
const MISSING_FOCUS = 'MISSING_FOCUS';
68-
const TYPE_PREFIX = '[Type] ';
69-
const FOCUS_PREFIX = '[Focus] ';
70-
const INFRASTRUCTURE_LABEL = 'Infrastructure';
71-
const PRIMARY_TYPE_LABELS = {
72-
'[Type] Feature': 'Features',
73-
'[Type] Enhancement': 'Enhancements',
74-
'[Type] Bug': 'Bug Fixes',
75-
};
76-
const PRIMARY_TYPE_ORDER = Object.values( PRIMARY_TYPE_LABELS );
77-
7873
/**
7974
* Returns a promise resolving to an array of pull requests associated with the
8075
* changelog settings object.
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/**
2+
* External dependencies
3+
*/
4+
const path = require( 'path' );
5+
const glob = require( 'fast-glob' );
6+
const fs = require( 'fs' );
7+
const { EOL } = require( 'os' );
8+
9+
/**
10+
* Internal dependencies
11+
*/
12+
const { log, formats } = require( '../lib/logger' );
13+
const config = require( '../config' );
14+
15+
const TAB = '\t';
16+
const NEWLINE = EOL;
17+
const FILE_HEADER = `<?php
18+
/* THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY. */
19+
$generated_i18n_strings = array(
20+
`;
21+
const FILE_FOOTER = `
22+
);
23+
/* THIS IS THE END OF THE GENERATED FILE */
24+
`;
25+
26+
/**
27+
* @typedef WPTranslationsCommandOptions
28+
*
29+
* @property {string=} directory Optional directory, default is the root `/modules` directory.
30+
* @property {string=} output Optional output PHP file, default is the root `/module-i18n.php` file.
31+
*/
32+
33+
/**
34+
* @typedef WPTranslationsSettings
35+
*
36+
* @property {string} textDomain Plugin text domain.
37+
* @property {string} directory Modules directory.
38+
* @property {string} output Output PHP file.
39+
*/
40+
41+
/**
42+
* @typedef WPTranslationEntry
43+
*
44+
* @property {string} text String to translate.
45+
* @property {string} context Context for translators.
46+
*/
47+
48+
exports.options = [
49+
{
50+
argname: '-d, --directory <directory>',
51+
description: 'Modules directory',
52+
},
53+
{
54+
argname: '-d, --output <output>',
55+
description: 'Output file',
56+
},
57+
];
58+
59+
/**
60+
* Command that generates a PHP file from module header translation strings.
61+
*
62+
* @param {WPTranslationsCommandOptions} opt
63+
*/
64+
exports.handler = async ( opt ) => {
65+
await createTranslations( {
66+
textDomain: config.textDomain,
67+
directory: opt.directory || 'modules',
68+
output: opt.output || 'module-i18n.php',
69+
} );
70+
};
71+
72+
/**
73+
* Parses module header translation strings.
74+
*
75+
* @param {WPTranslationsSettings} settings Translations settings.
76+
*
77+
* @return {[]WPTranslationEntry} List of translation entries.
78+
*/
79+
async function getTranslations( settings ) {
80+
const moduleFilePattern = path.join( settings.directory, '*/load.php' );
81+
const moduleFiles = await glob( path.resolve( '.', moduleFilePattern ) );
82+
83+
const moduleTranslations = moduleFiles
84+
.map( ( moduleFile ) => {
85+
// Map of module header => translator context.
86+
const headers = {
87+
'Module Name': 'module name',
88+
Description: 'module description',
89+
};
90+
const translationEntries = [];
91+
92+
const fileContent = fs.readFileSync( moduleFile, 'utf8' );
93+
const regex = new RegExp(
94+
`^(?:[ \t]*<?php)?[ \t/*#@]*(${ Object.keys( headers ).join(
95+
'|'
96+
) }):(.*)$`,
97+
'gmi'
98+
);
99+
let match = regex.exec( fileContent );
100+
while ( match ) {
101+
const text = match[ 2 ].trim();
102+
const context = headers[ match[ 1 ] ];
103+
if ( text && context ) {
104+
translationEntries.push( {
105+
text,
106+
context,
107+
} );
108+
}
109+
match = regex.exec( fileContent );
110+
}
111+
112+
return translationEntries;
113+
} )
114+
.filter( ( translations ) => !! translations.length );
115+
116+
return moduleTranslations.flat();
117+
}
118+
119+
/**
120+
* Parses module header translation strings.
121+
*
122+
* @param {[]WPTranslationEntry} translations List of translation entries.
123+
* @param {WPTranslationsSettings} settings Translations settings.
124+
*/
125+
function createTranslationsPHPFile( translations, settings ) {
126+
const output = translations.map( ( translation ) => {
127+
// Escape single quotes.
128+
return `${ TAB }_x( '${ translation.text.replace( /'/g, "\\'" ) }', '${
129+
translation.context
130+
}', '${ settings.textDomain }' ),`;
131+
} );
132+
133+
const fileOutput = `${ FILE_HEADER }${ output.join(
134+
NEWLINE
135+
) }${ FILE_FOOTER }`;
136+
fs.writeFileSync( path.join( '.', settings.output ), fileOutput );
137+
}
138+
139+
/**
140+
* Parses module header translation strings and generates a PHP file with them.
141+
*
142+
* @param {WPTranslationsSettings} settings Translations settings.
143+
*/
144+
async function createTranslations( settings ) {
145+
log(
146+
formats.title(
147+
`\n💃Preparing module translations for "${ settings.directory }" in "${ settings.output }"\n\n`
148+
)
149+
);
150+
151+
try {
152+
const translations = await getTranslations( settings );
153+
createTranslationsPHPFile( translations, settings );
154+
} catch ( error ) {
155+
if ( error instanceof Error ) {
156+
log( formats.error( error.stack ) );
157+
return;
158+
}
159+
}
160+
161+
log(
162+
formats.success(
163+
`\n💃Module translations successfully set in "${ settings.output }"\n\n`
164+
)
165+
);
166+
}

bin/plugin/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
const config = {
1212
githubRepositoryOwner: 'WordPress',
1313
githubRepositoryName: 'performance',
14+
textDomain: 'performance-lab',
1415
};
1516

1617
module.exports = config;

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@
2424
"@wordpress/scripts": "^19.0",
2525
"chalk": "4.1.1",
2626
"commander": "4.1.0",
27+
"fast-glob": "^3.2.7",
2728
"lodash": "4.17.21"
2829
},
2930
"scripts": {
3031
"changelog": "./bin/plugin/cli.js changelog",
32+
"translations": "./bin/plugin/cli.js translations",
3133
"format-js": "wp-scripts format ./bin",
3234
"lint-js": "wp-scripts lint-js ./bin",
3335
"format-php": "wp-env run composer run-script format",

0 commit comments

Comments
 (0)