xarxaprod-wp-theme/vendor/wp-cli/i18n-command/src/MakePotCommand.php

988 lines
31 KiB
PHP
Raw Normal View History

2024-01-09 16:13:20 +01:00
<?php
namespace WP_CLI\I18n;
use Gettext\Extractors\Po;
use Gettext\Merge;
use Gettext\Translation;
use Gettext\Translations;
use Gettext\Utils\ParsedComment;
use WP_CLI;
use WP_CLI_Command;
use WP_CLI\Utils;
use DirectoryIterator;
use IteratorIterator;
class MakePotCommand extends WP_CLI_Command {
/**
* @var string
*/
protected $source;
/**
* @var string
*/
protected $destination;
/**
* @var array
*/
protected $merge = [];
/**
* @var Translations[]
*/
protected $exceptions = [];
/**
* @var bool
*/
protected $subtract_and_merge;
/**
* @var array
*/
protected $include = [];
/**
* @var array
*/
protected $exclude = [ 'node_modules', '.git', '.svn', '.CVS', '.hg', 'vendor', 'Gruntfile.js', 'webpack.config.js', '*.min.js' ];
/**
* @var string
*/
protected $slug;
/**
* @var array
*/
protected $main_file_data = [];
/**
* @var bool
*/
protected $skip_js = false;
/**
* @var bool
*/
protected $skip_php = false;
/**
* @var bool
*/
protected $skip_blade = false;
/**
* @var bool
*/
protected $skip_block_json = false;
/**
* @var bool
*/
protected $skip_theme_json = false;
/**
* @var bool
*/
protected $skip_audit = false;
/**
* @var bool
*/
protected $location = true;
/**
* @var array
*/
protected $headers = [];
/**
* @var string
*/
protected $domain;
/**
* @var string
*/
protected $package_name;
/**
* @var string
*/
protected $file_comment;
/**
* @var string
*/
protected $project_type = 'generic';
/**
* These Regexes copied from http://php.net/manual/en/function.sprintf.php#93552
* and adjusted for better precision and updated specs.
*/
const SPRINTF_PLACEHOLDER_REGEX = '/(?:
(?<!%) # Don\'t match a literal % (%%).
(
% # Start of placeholder.
(?:[0-9]+\$)? # Optional ordering of the placeholders.
[+-]? # Optional sign specifier.
(?:
(?:0|\'.)? # Optional padding specifier - excluding the space.
-? # Optional alignment specifier.
[0-9]* # Optional width specifier.
(?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
| # Only recognize the space as padding in combination with a width specifier.
(?:[ ])? # Optional space padding specifier.
-? # Optional alignment specifier.
[0-9]+ # Width specifier.
(?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
)
[bcdeEfFgGosuxX] # Type specifier.
)
)/x';
/**
* "Unordered" means there's no position specifier: '%s', not '%2$s'.
*/
const UNORDERED_SPRINTF_PLACEHOLDER_REGEX = '/(?:
(?<!%) # Don\'t match a literal % (%%).
% # Start of placeholder.
[+-]? # Optional sign specifier.
(?:
(?:0|\'.)? # Optional padding specifier - excluding the space.
-? # Optional alignment specifier.
[0-9]* # Optional width specifier.
(?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
| # Only recognize the space as padding in combination with a width specifier.
(?:[ ])? # Optional space padding specifier.
-? # Optional alignment specifier.
[0-9]+ # Width specifier.
(?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
)
[bcdeEfFgGosuxX] # Type specifier.
)/x';
/**
* Create a POT file for a WordPress project.
*
* Scans PHP, Blade-PHP and JavaScript files for translatable strings, as well as theme stylesheets and plugin files
* if the source directory is detected as either a plugin or theme.
*
* ## OPTIONS
*
* <source>
* : Directory to scan for string extraction.
*
* [<destination>]
* : Name of the resulting POT file.
*
* [--slug=<slug>]
* : Plugin or theme slug. Defaults to the source directory's basename.
*
* [--domain=<domain>]
* : Text domain to look for in the source code, unless the `--ignore-domain` option is used.
* By default, the "Text Domain" header of the plugin or theme is used.
* If none is provided, it falls back to the project slug.
*
* [--ignore-domain]
* : Ignore the text domain completely and extract strings with any text domain.
*
* [--merge[=<paths>]]
* : Comma-separated list of POT files whose contents should be merged with the extracted strings.
* If left empty, defaults to the destination POT file. POT file headers will be ignored.
*
* [--subtract=<paths>]
* : Comma-separated list of POT files whose contents should act as some sort of denylist for string extraction.
* Any string which is found on that denylist will not be extracted.
* This can be useful when you want to create multiple POT files from the same source directory with slightly
* different content and no duplicate strings between them.
*
* [--subtract-and-merge]
* : Whether source code references and comments from the generated POT file should be instead added to the POT file
* used for subtraction. Warning: this modifies the files passed to `--subtract`!
*
* [--include=<paths>]
* : Comma-separated list of files and paths that should be used for string extraction.
* If provided, only these files and folders will be taken into account for string extraction.
* For example, `--include="src,my-file.php` will ignore anything besides `my-file.php` and files in the `src`
* directory. Simple glob patterns can be used, i.e. `--include=foo-*.php` includes any PHP file with the `foo-`
* prefix. Leading and trailing slashes are ignored, i.e. `/my/directory/` is the same as `my/directory`.
*
* [--exclude=<paths>]
* : Comma-separated list of files and paths that should be skipped for string extraction.
* For example, `--exclude=".github,myfile.php` would ignore any strings found within `myfile.php` or the `.github`
* folder. Simple glob patterns can be used, i.e. `--exclude=foo-*.php` excludes any PHP file with the `foo-`
* prefix. Leading and trailing slashes are ignored, i.e. `/my/directory/` is the same as `my/directory`. The
* following files and folders are always excluded: node_modules, .git, .svn, .CVS, .hg, vendor, *.min.js.
*
* [--headers=<headers>]
* : Array in JSON format of custom headers which will be added to the POT file. Defaults to empty array.
*
* [--location]
* : Whether to write `#: filename:line` lines.
* Defaults to true, use `--no-location` to skip the removal.
* Note that disabling this option makes it harder for technically skilled translators to understand each messages context.
*
* [--skip-js]
* : Skips JavaScript string extraction. Useful when this is done in another build step, e.g. through Babel.
*
* [--skip-php]
* : Skips PHP string extraction.
*
* [--skip-blade]
* : Skips Blade-PHP string extraction.
*
* [--skip-block-json]
* : Skips string extraction from block.json files.
*
* [--skip-theme-json]
* : Skips string extraction from theme.json files.
*
* [--skip-audit]
* : Skips string audit where it tries to find possible mistakes in translatable strings. Useful when running in an
* automated environment.
*
* [--file-comment=<file-comment>]
* : String that should be added as a comment to the top of the resulting POT file.
* By default, a copyright comment is added for WordPress plugins and themes in the following manner:
*
* ```
* Copyright (C) 2018 Example Plugin Author
* This file is distributed under the same license as the Example Plugin package.
* ```
*
* If a plugin or theme specifies a license in their main plugin file or stylesheet, the comment looks like
* this:
*
* ```
* Copyright (C) 2018 Example Plugin Author
* This file is distributed under the GPLv2.
* ```
*
* [--package-name=<name>]
* : Name to use for package name in the resulting POT file's `Project-Id-Version` header.
* Overrides plugin or theme name, if applicable.
*
* ## EXAMPLES
*
* # Create a POT file for the WordPress plugin/theme in the current directory
* $ wp i18n make-pot . languages/my-plugin.pot
*
* # Create a POT file for the continents and cities list in WordPress core.
* $ wp i18n make-pot . continents-and-cities.pot --include="wp-admin/includes/continents-cities.php"
* --ignore-domain
*
* @when before_wp_load
*
* @throws WP_CLI\ExitException
*/
public function __invoke( $args, $assoc_args ) {
$this->handle_arguments( $args, $assoc_args );
$translations = $this->extract_strings();
if ( ! $translations ) {
WP_CLI::warning( 'No strings found' );
}
$translations_count = count( $translations );
if ( 1 === $translations_count ) {
WP_CLI::debug( sprintf( 'Extracted %d string', $translations_count ), 'make-pot' );
} else {
WP_CLI::debug( sprintf( 'Extracted %d strings', $translations_count ), 'make-pot' );
}
if ( ! PotGenerator::toFile( $translations, $this->destination ) ) {
WP_CLI::error( 'Could not generate a POT file!' );
}
WP_CLI::success( 'POT file successfully generated!' );
}
/**
* Process arguments from command-line in a reusable way.
*
* @throws WP_CLI\ExitException
*
* @param array $args Command arguments.
* @param array $assoc_args Associative arguments.
*/
public function handle_arguments( $args, $assoc_args ) {
$array_arguments = array( 'headers' );
$assoc_args = Utils\parse_shell_arrays( $assoc_args, $array_arguments );
$this->source = realpath( $args[0] );
$this->slug = Utils\get_flag_value( $assoc_args, 'slug', Utils\basename( $this->source ) );
$this->skip_js = Utils\get_flag_value( $assoc_args, 'skip-js', $this->skip_js );
$this->skip_php = Utils\get_flag_value( $assoc_args, 'skip-php', $this->skip_php );
$this->skip_blade = Utils\get_flag_value( $assoc_args, 'skip-blade', $this->skip_blade );
$this->skip_block_json = Utils\get_flag_value( $assoc_args, 'skip-block-json', $this->skip_block_json );
$this->skip_theme_json = Utils\get_flag_value( $assoc_args, 'skip-theme-json', $this->skip_theme_json );
$this->skip_audit = Utils\get_flag_value( $assoc_args, 'skip-audit', $this->skip_audit );
$this->headers = Utils\get_flag_value( $assoc_args, 'headers', $this->headers );
$this->file_comment = Utils\get_flag_value( $assoc_args, 'file-comment' );
$this->package_name = Utils\get_flag_value( $assoc_args, 'package-name' );
$this->location = Utils\get_flag_value( $assoc_args, 'location', true );
$ignore_domain = Utils\get_flag_value( $assoc_args, 'ignore-domain', false );
if ( ! $this->source || ! is_dir( $this->source ) ) {
WP_CLI::error( 'Not a valid source directory!' );
}
$this->main_file_data = $this->get_main_file_data();
if ( $ignore_domain ) {
WP_CLI::debug( 'Extracting all strings regardless of text domain', 'make-pot' );
}
if ( ! $ignore_domain ) {
$this->domain = $this->slug;
if ( ! empty( $this->main_file_data['Text Domain'] ) ) {
$this->domain = $this->main_file_data['Text Domain'];
}
$this->domain = Utils\get_flag_value( $assoc_args, 'domain', $this->domain );
WP_CLI::debug( sprintf( 'Extracting all strings with text domain "%s"', $this->domain ), 'make-pot' );
}
// Determine destination.
$this->destination = "{$this->source}/{$this->slug}.pot";
if ( ! empty( $this->main_file_data['Domain Path'] ) ) {
// Domain Path inside source folder.
$this->destination = sprintf(
'%s/%s/%s.pot',
$this->source,
$this->unslashit( $this->main_file_data['Domain Path'] ),
$this->slug
);
}
if ( isset( $args[1] ) ) {
$this->destination = $args[1];
}
WP_CLI::debug( sprintf( 'Destination: %s', $this->destination ), 'make-pot' );
// Two is_dir() checks in case of a race condition.
if ( ! is_dir( dirname( $this->destination ) )
&& ! mkdir( dirname( $this->destination ), 0777, true )
&& ! is_dir( dirname( $this->destination ) )
) {
WP_CLI::error( 'Could not create destination directory!' );
}
if ( isset( $assoc_args['merge'] ) ) {
if ( true === $assoc_args['merge'] ) {
$this->merge = [ $this->destination ];
} elseif ( ! empty( $assoc_args['merge'] ) ) {
$this->merge = explode( ',', $assoc_args['merge'] );
}
$this->merge = array_filter(
$this->merge,
function ( $file ) {
if ( ! file_exists( $file ) ) {
WP_CLI::warning( sprintf( 'Invalid file provided to --merge: %s', $file ) );
return false;
}
return true;
}
);
if ( ! empty( $this->merge ) ) {
WP_CLI::debug(
sprintf(
'Merging with existing POT %s: %s',
WP_CLI\Utils\pluralize( 'file', count( $this->merge ) ),
implode( ',', $this->merge )
),
'make-pot'
);
}
}
if ( isset( $assoc_args['subtract'] ) ) {
$this->subtract_and_merge = Utils\get_flag_value( $assoc_args, 'subtract-and-merge', false );
$files = explode( ',', $assoc_args['subtract'] );
foreach ( $files as $file ) {
if ( ! file_exists( $file ) ) {
WP_CLI::warning( sprintf( 'Invalid file provided to --subtract: %s', $file ) );
continue;
}
WP_CLI::debug( sprintf( 'Ignoring any string already existing in: %s', $file ), 'make-pot' );
$this->exceptions[ $file ] = new Translations();
Po::fromFile( $file, $this->exceptions[ $file ] );
}
}
if ( isset( $assoc_args['include'] ) ) {
$this->include = array_filter( explode( ',', $assoc_args['include'] ) );
$this->include = array_map( [ $this, 'unslashit' ], $this->include );
$this->include = array_unique( $this->include );
WP_CLI::debug( sprintf( 'Only including the following files: %s', implode( ',', $this->include ) ), 'make-pot' );
}
if ( isset( $assoc_args['exclude'] ) ) {
$this->exclude = array_filter( array_merge( $this->exclude, explode( ',', $assoc_args['exclude'] ) ) );
$this->exclude = array_map( [ $this, 'unslashit' ], $this->exclude );
$this->exclude = array_unique( $this->exclude );
}
WP_CLI::debug( sprintf( 'Excluding the following files: %s', implode( ',', $this->exclude ) ), 'make-pot' );
}
/**
* Removes leading and trailing slashes of a string.
*
* @param string $string What to add and remove slashes from.
* @return string String without leading and trailing slashes.
*/
protected function unslashit( $string ) {
return ltrim( rtrim( trim( $string ), '/\\' ), '/\\' );
}
/**
* Retrieves the main file data of the plugin or theme.
*
* @return array
*/
protected function get_main_file_data() {
$files = new IteratorIterator( new DirectoryIterator( $this->source ) );
/** @var DirectoryIterator $file */
foreach ( $files as $file ) {
// wp-content/themes/my-theme/style.css
if ( $file->isFile() && 'style' === $file->getBasename( '.css' ) && $file->isReadable() ) {
$theme_data = FileDataExtractor::get_file_data(
$file->getRealPath(),
array_combine(
$this->get_file_headers( 'theme' ),
$this->get_file_headers( 'theme' )
)
);
// Stop when it contains a valid Theme Name header.
if ( ! empty( $theme_data['Theme Name'] ) ) {
WP_CLI::log( 'Theme stylesheet detected.' );
WP_CLI::debug( sprintf( 'Theme stylesheet: %s', $file->getRealPath() ), 'make-pot' );
$this->project_type = 'theme';
return $theme_data;
}
}
// wp-content/themes/my-themes/theme-a/style.css
if ( $file->isDir() && ! $file->isDot() && is_readable( $file->getRealPath() . '/style.css' ) ) {
$theme_data = FileDataExtractor::get_file_data(
$file->getRealPath() . '/style.css',
array_combine(
$this->get_file_headers( 'theme' ),
$this->get_file_headers( 'theme' )
)
);
// Stop when it contains a valid Theme Name header.
if ( ! empty( $theme_data['Theme Name'] ) ) {
WP_CLI::log( 'Theme stylesheet detected.' );
WP_CLI::debug( sprintf( 'Theme stylesheet: %s', $file->getRealPath() . '/style.css' ), 'make-pot' );
$this->project_type = 'theme';
return $theme_data;
}
}
// wp-content/plugins/my-plugin/my-plugin.php
if ( $file->isFile() && $file->isReadable() && 'php' === $file->getExtension() ) {
$plugin_data = FileDataExtractor::get_file_data(
$file->getRealPath(),
array_combine(
$this->get_file_headers( 'plugin' ),
$this->get_file_headers( 'plugin' )
)
);
// Stop when we find a file with a valid Plugin Name header.
if ( ! empty( $plugin_data['Plugin Name'] ) ) {
WP_CLI::log( 'Plugin file detected.' );
WP_CLI::debug( sprintf( 'Plugin file: %s', $file->getRealPath() ), 'make-pot' );
$this->project_type = 'plugin';
return $plugin_data;
}
}
}
WP_CLI::debug( 'No valid theme stylesheet or plugin file found, treating as a regular project.', 'make-pot' );
return [];
}
/**
* Returns the file headers for themes and plugins.
*
* @param string $type Source type, either theme or plugin.
*
* @return array List of file headers.
*/
protected function get_file_headers( $type ) {
switch ( $type ) {
case 'plugin':
return [
'Plugin Name',
'Plugin URI',
'Description',
'Author',
'Author URI',
'Version',
'License',
'Domain Path',
'Text Domain',
];
case 'theme':
return [
'Theme Name',
'Theme URI',
'Description',
'Author',
'Author URI',
'Version',
'License',
'Domain Path',
'Text Domain',
];
default:
return [];
}
}
/**
* Creates a POT file and stores it on disk.
*
* @throws WP_CLI\ExitException
*
* @return Translations A Translation set.
*/
protected function extract_strings() {
$translations = new Translations();
// Add existing strings first but don't keep headers.
if ( ! empty( $this->merge ) ) {
$existing_translations = new Translations();
Po::fromFile( $this->merge, $existing_translations );
$translations->mergeWith( $existing_translations, Merge::ADD | Merge::REMOVE );
}
PotGenerator::setCommentBeforeHeaders( $this->get_file_comment() );
$this->set_default_headers( $translations );
// POT files have no Language header.
$translations->deleteHeader( Translations::HEADER_LANGUAGE );
// Only relevant for PO files, not POT files.
$translations->setHeader( 'PO-Revision-Date', 'YEAR-MO-DA HO:MI+ZONE' );
if ( $this->domain ) {
$translations->setDomain( $this->domain );
}
unset( $this->main_file_data['Version'], $this->main_file_data['License'], $this->main_file_data['Domain Path'], $this->main_file_data['Text Domain'] );
$is_theme = isset( $this->main_file_data['Theme Name'] );
// Set entries from main file data.
foreach ( $this->main_file_data as $header => $data ) {
if ( empty( $data ) ) {
continue;
}
$translation = new Translation( '', $data );
if ( $is_theme ) {
$translation->addExtractedComment( sprintf( '%s of the theme', $header ) );
} else {
$translation->addExtractedComment( sprintf( '%s of the plugin', $header ) );
}
$translations[] = $translation;
}
try {
if ( ! $this->skip_php ) {
$options = [
// Extract 'Template Name' headers in theme files.
'wpExtractTemplates' => $is_theme,
// Extract 'Title' and 'Description' headers from pattern files.
'wpExtractPatterns' => $is_theme,
'include' => $this->include,
'exclude' => $this->exclude,
'extensions' => [ 'php' ],
'addReferences' => $this->location,
];
PhpCodeExtractor::fromDirectory( $this->source, $translations, $options );
}
if ( ! $this->skip_blade ) {
$options = [
'include' => $this->include,
'exclude' => $this->exclude,
'extensions' => [ 'blade.php' ],
'addReferences' => $this->location,
];
BladeCodeExtractor::fromDirectory( $this->source, $translations, $options );
}
if ( ! $this->skip_js ) {
JsCodeExtractor::fromDirectory(
$this->source,
$translations,
[
'include' => $this->include,
'exclude' => $this->exclude,
'extensions' => [ 'js', 'jsx' ],
'addReferences' => $this->location,
]
);
MapCodeExtractor::fromDirectory(
$this->source,
$translations,
[
'include' => $this->include,
'exclude' => $this->exclude,
'extensions' => [ 'map' ],
'addReferences' => $this->location,
]
);
}
if ( ! $this->skip_block_json ) {
BlockExtractor::fromDirectory(
$this->source,
$translations,
[
'schema' => JsonSchemaExtractor::BLOCK_JSON_SOURCE,
'schemaFallback' => JsonSchemaExtractor::BLOCK_JSON_FALLBACK,
// Only look for block.json files, nothing else.
'restrictFileNames' => [ 'block.json' ],
'include' => $this->include,
'exclude' => $this->exclude,
'extensions' => [ 'json' ],
'addReferences' => $this->location,
]
);
}
if ( ! $this->skip_theme_json ) {
// This will look for the top-level theme.json file, as well as
// any JSON file within the top-level styles/ directory.
ThemeJsonExtractor::fromDirectory(
$this->source,
$translations,
[
'schema' => JsonSchemaExtractor::THEME_JSON_SOURCE,
'schemaFallback' => JsonSchemaExtractor::THEME_JSON_FALLBACK,
'include' => $this->include,
'exclude' => $this->exclude,
'extensions' => [ 'json' ],
'addReferences' => $this->location,
]
);
}
} catch ( \Exception $e ) {
WP_CLI::error( $e->getMessage() );
}
foreach ( $this->exceptions as $file => $exception_translations ) {
/** @var Translation $exception_translation */
foreach ( $exception_translations as $exception_translation ) {
if ( ! $translations->find( $exception_translation ) ) {
continue;
}
if ( $this->subtract_and_merge ) {
$translation = $translations[ $exception_translation->getId() ];
$exception_translation->mergeWith( $translation );
}
unset( $translations[ $exception_translation->getId() ] );
}
if ( $this->subtract_and_merge ) {
PotGenerator::toFile( $exception_translations, $file );
}
}
if ( ! $this->skip_audit ) {
$this->audit_strings( $translations );
}
return $translations;
}
/**
* Audits strings.
*
* Goes through all extracted strings to find possible mistakes.
*
* @param Translations $translations Translations object.
*/
protected function audit_strings( $translations ) {
foreach ( $translations as $translation ) {
/** @var Translation $translation */
$references = $translation->getReferences();
// File headers don't have any file references.
$location = $translation->hasReferences() ? '(' . implode( ':', $references[0] ) . ')' : '';
// Check 1: Flag strings with placeholders that should have translator comments.
if (
! $translation->hasExtractedComments() &&
preg_match( self::SPRINTF_PLACEHOLDER_REGEX, $translation->getOriginal(), $placeholders ) >= 1
) {
$message = sprintf(
'The string "%1$s" contains placeholders but has no "translators:" comment to clarify their meaning. %2$s',
$translation->getOriginal(),
$location
);
WP_CLI::warning( $message );
}
// Check 2: Flag strings with different translator comments.
if ( $translation->hasExtractedComments() ) {
$comments = $translation->getExtractedComments();
// Remove plugin header information from comments.
$comments = array_filter(
$comments,
function ( $comment ) {
/** @var ParsedComment|string $comment */
/** @var string $file_header */
foreach ( $this->get_file_headers( $this->project_type ) as $file_header ) {
if ( 0 === strpos( ( $comment instanceof ParsedComment ? $comment->getComment() : $comment ), $file_header ) ) {
return null;
}
}
return $comment;
}
);
$unique_comments = array();
// Remove duplicate comments.
$comments = array_filter(
$comments,
function ( $comment ) use ( &$unique_comments ) {
/** @var ParsedComment|string $comment */
if ( in_array( ( $comment instanceof ParsedComment ? $comment->getComment() : $comment ), $unique_comments, true ) ) {
return null;
}
$unique_comments[] = ( $comment instanceof ParsedComment ? $comment->getComment() : $comment );
return $comment;
}
);
$comments_count = count( $comments );
if ( $comments_count > 1 ) {
$message = sprintf(
"The string \"%1\$s\" has %2\$d different translator comments. %3\$s\n%4\$s",
$translation->getOriginal(),
$comments_count,
$location,
implode( "\n", $unique_comments )
);
WP_CLI::warning( $message );
}
}
$non_placeholder_content = trim( preg_replace( '`^([\'"])(.*)\1$`Ds', '$2', $translation->getOriginal() ) );
$non_placeholder_content = preg_replace( self::SPRINTF_PLACEHOLDER_REGEX, '', $non_placeholder_content );
// Check 3: Flag empty strings without any translatable content.
if ( '' === $non_placeholder_content ) {
$message = sprintf(
'Found string without translatable content. %s',
$location
);
WP_CLI::warning( $message );
}
// Check 4: Flag strings with multiple unordered placeholders (%s %s %s vs. %1$s %2$s %3$s).
$unordered_matches_count = preg_match_all( self::UNORDERED_SPRINTF_PLACEHOLDER_REGEX, $translation->getOriginal(), $unordered_matches );
$unordered_matches = $unordered_matches[0];
if ( $unordered_matches_count >= 2 ) {
$message = sprintf(
'Multiple placeholders should be ordered. %s',
$location
);
WP_CLI::warning( $message );
}
if ( $translation->hasPlural() ) {
preg_match_all( self::SPRINTF_PLACEHOLDER_REGEX, $translation->getOriginal(), $single_placeholders );
$single_placeholders = $single_placeholders[0];
preg_match_all( self::SPRINTF_PLACEHOLDER_REGEX, $translation->getPlural(), $plural_placeholders );
$plural_placeholders = $plural_placeholders[0];
// see https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#plurals
if ( count( $single_placeholders ) < count( $plural_placeholders ) ) {
// Check 5: Flag things like _n( 'One comment', '%s Comments' )
$message = sprintf(
'Missing singular placeholder, needed for some languages. See https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#plurals %s',
$location
);
WP_CLI::warning( $message );
} else {
// Reordering is fine, but mismatched placeholders is probably wrong.
sort( $single_placeholders );
sort( $plural_placeholders );
// Check 6: Flag things like _n( '%s Comment (%d)', '%s Comments (%s)' )
if ( $single_placeholders !== $plural_placeholders ) {
$message = sprintf(
'Mismatched placeholders for singular and plural string. %s',
$location
);
WP_CLI::warning( $message );
}
}
}
}
}
/**
* Returns the copyright comment for the given package.
*
* @return string File comment.
*/
protected function get_file_comment() {
if ( '' === $this->file_comment ) {
return '';
}
if ( isset( $this->file_comment ) ) {
return implode( "\n", explode( '\n', $this->file_comment ) );
}
if ( isset( $this->main_file_data['Theme Name'] ) ) {
if ( isset( $this->main_file_data['License'] ) ) {
return sprintf(
"Copyright (C) %1\$s %2\$s\nThis file is distributed under the %3\$s.",
date( 'Y' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
$this->main_file_data['Author'],
$this->main_file_data['License']
);
}
return sprintf(
"Copyright (C) %1\$s %2\$s\nThis file is distributed under the same license as the %3\$s theme.",
date( 'Y' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
$this->main_file_data['Author'],
$this->main_file_data['Theme Name']
);
}
if ( isset( $this->main_file_data['Plugin Name'] ) ) {
if ( isset( $this->main_file_data['License'] ) && ! empty( $this->main_file_data['License'] ) ) {
return sprintf(
"Copyright (C) %1\$s %2\$s\nThis file is distributed under the %3\$s.",
date( 'Y' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
$this->main_file_data['Author'],
$this->main_file_data['License']
);
}
return sprintf(
"Copyright (C) %1\$s %2\$s\nThis file is distributed under the same license as the %3\$s plugin.",
date( 'Y' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
$this->main_file_data['Author'],
$this->main_file_data['Plugin Name']
);
}
return '';
}
/**
* Sets default POT file headers for the project.
*
* @param Translations $translations Translations object.
*/
protected function set_default_headers( $translations ) {
$name = null;
$version = $this->get_wp_version();
$bugs_address = null;
if ( ! $version && isset( $this->main_file_data['Version'] ) ) {
$version = $this->main_file_data['Version'];
}
if ( isset( $this->main_file_data['Theme Name'] ) ) {
$name = $this->main_file_data['Theme Name'];
$bugs_address = sprintf( 'https://wordpress.org/support/theme/%s', $this->slug );
} elseif ( isset( $this->main_file_data['Plugin Name'] ) ) {
$name = $this->main_file_data['Plugin Name'];
$bugs_address = sprintf( 'https://wordpress.org/support/plugin/%s', $this->slug );
}
if ( null !== $this->package_name ) {
$name = $this->package_name;
}
if ( null !== $name ) {
$translations->setHeader( 'Project-Id-Version', $name . ( $version ? ' ' . $version : '' ) );
}
if ( null !== $bugs_address ) {
$translations->setHeader( 'Report-Msgid-Bugs-To', $bugs_address );
}
$translations->setHeader( 'Last-Translator', 'FULL NAME <EMAIL@ADDRESS>' );
$translations->setHeader( 'Language-Team', 'LANGUAGE <LL@li.org>' );
$translations->setHeader( 'X-Generator', 'WP-CLI ' . WP_CLI_VERSION );
foreach ( $this->headers as $key => $value ) {
$translations->setHeader( $key, $value );
}
}
/**
* Extracts the WordPress version number from wp-includes/version.php.
*
* @return string|false Version number on success, false otherwise.
*/
protected function get_wp_version() {
$version_php = $this->source . '/wp-includes/version.php';
if ( ! file_exists( $version_php ) || ! is_readable( $version_php ) ) {
return false;
}
return preg_match( '/\$wp_version\s*=\s*\'(.*?)\';/', file_get_contents( $version_php ), $matches ) ? $matches[1] : false;
}
}