* : Directory to scan for string extraction. * * [] * : Name of the resulting POT file. * * [--slug=] * : Plugin or theme slug. Defaults to the source directory's basename. * * [--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[=]] * : 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=] * : 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=] * : 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=] * : 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=] * : 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 message’s 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=] * : 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 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 ' ); $translations->setHeader( 'Language-Team', 'LANGUAGE ' ); $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; } }