diff --git a/features/makepot.feature b/features/makepot.feature
index 9d7bba31..f86d0131 100644
--- a/features/makepot.feature
+++ b/features/makepot.feature
@@ -3092,6 +3092,69 @@ Feature: Generate a POT file of a WordPress project
msgid "Page not found."
"""
+ @blade
+ Scenario: Extract strings from Blade component prop bindings
+ Given an empty foo-theme directory
+ And a foo-theme/style.css file:
+ """
+ /*
+ Theme Name: Foo Theme
+ Theme URI: https://example.com
+ Description:
+ Author:
+ Author URI:
+ Version: 0.1.0
+ License: GPL-2.0+
+ Text Domain: foo-theme
+ */
+ """
+ And a foo-theme/components.blade.php file:
+ """
+ @extends('layouts.app')
+
+ @section('content')
+
+
+
+
+
+ {!! __('Card content', 'foo-theme') !!}
+
+
+ {{ __('Regular echo', 'foo-theme') }}
+ @endsection
+ """
+
+ When I try `wp i18n make-pot foo-theme result.pot --debug`
+ Then STDOUT should be:
+ """
+ Theme stylesheet detected.
+ Success: POT file successfully generated.
+ """
+ And the result.pot file should contain:
+ """
+ msgid "Bound prop string"
+ """
+ And the result.pot file should contain:
+ """
+ msgid "No results found"
+ """
+ And the result.pot file should contain:
+ """
+ msgid "Please try a new search"
+ """
+ And the result.pot file should contain:
+ """
+ msgid "Card content"
+ """
+ And the result.pot file should contain:
+ """
+ msgid "Regular echo"
+ """
+
Scenario: Custom package name
Given an empty example-project directory
And a example-project/stuff.php file:
diff --git a/src/BladeGettextExtractor.php b/src/BladeGettextExtractor.php
index 58518bd5..4fdd0558 100644
--- a/src/BladeGettextExtractor.php
+++ b/src/BladeGettextExtractor.php
@@ -39,13 +39,48 @@ protected static function compileBladeToPhp( $text ) {
return static::getBladeCompiler()->compileString( $text );
}
+ /**
+ * Extracts PHP expressions from Blade component prop bindings.
+ *
+ * BladeOne does not compile tags, so bound prop
+ * expressions like :prop="__('text', 'domain')" are left as-is
+ * and invisible to the PHP scanner. This method extracts those
+ * expressions and returns them as PHP code so that any gettext
+ * function calls within them can be detected.
+ *
+ * @param string $text Blade template string.
+ * @return string PHP code containing the extracted expressions.
+ */
+ protected static function extractComponentPropExpressions( $text ) {
+ $php = '';
+
+ // Match opening (and self-closing) Blade component tags: or
+ // The attribute region handles quoted strings so that a '>' inside an
+ // attribute value does not end the match prematurely.
+ if ( ! preg_match_all( '/"\']*(?:"[^"]*"|\'[^\']*\'))*[^>"]*)\/?>/', $text, $tag_matches ) ) {
+ return $php;
+ }
+
+ foreach ( $tag_matches[1] as $attributes ) {
+ // Find :prop="expression" or :prop='expression' bound attributes.
+ if ( preg_match_all( '/(?';
+ }
+ }
+ }
+
+ return $php;
+ }
+
/**
* {@inheritdoc}
*
* Note: In the parent PhpCode class fromString() uses fromStringMultiple() (overridden here)
*/
public static function fromStringMultiple( $text, array $translations, array $options = [] ) {
- $php_string = static::compileBladeToPhp( $text );
+ $php_string = static::compileBladeToPhp( $text );
+ $php_string .= static::extractComponentPropExpressions( $text );
return parent::fromStringMultiple( $php_string, $translations, $options );
}
}
diff --git a/tests/BladeGettextExtractorTest.php b/tests/BladeGettextExtractorTest.php
new file mode 100644
index 00000000..3b44fa5a
--- /dev/null
+++ b/tests/BladeGettextExtractorTest.php
@@ -0,0 +1,143 @@
+setDomain( $domain );
+
+ $options = array_merge(
+ BladeCodeExtractor::$options,
+ [ 'file' => 'test.blade.php' ]
+ );
+
+ BladeCodeExtractor::fromString( $blade, $translations, $options );
+
+ return $translations;
+ }
+
+ public function test_extracts_bound_prop_with_translation_function() {
+ $translations = $this->extract(
+ ''
+ );
+
+ $this->assertNotFalse( $translations->find( null, 'Hello' ) );
+ }
+
+ public function test_extracts_multiple_bound_props() {
+ $translations = $this->extract(
+ ''
+ );
+
+ $this->assertNotFalse( $translations->find( null, 'Not found' ) );
+ $this->assertNotFalse( $translations->find( null, 'Try again' ) );
+ }
+
+ public function test_extracts_bound_props_from_multiline_component_tag() {
+ $blade = <<<'BLADE'
+
+BLADE;
+
+ $translations = $this->extract( $blade );
+
+ $this->assertNotFalse( $translations->find( null, 'Page not found' ) );
+ $this->assertNotFalse( $translations->find( null, 'Please try again' ) );
+ }
+
+ public function test_extracts_bound_props_from_open_component_tag() {
+ $blade = <<<'BLADE'
+
+ {!! __('Content inside', 'foo-theme') !!}
+
+BLADE;
+
+ $translations = $this->extract( $blade );
+
+ $this->assertNotFalse( $translations->find( null, 'Warning message' ) );
+ $this->assertNotFalse( $translations->find( null, 'Content inside' ) );
+ }
+
+ public function test_ignores_static_props() {
+ $translations = $this->extract(
+ ''
+ );
+
+ $this->assertNotFalse( $translations->find( null, 'Hello' ) );
+ $this->assertFalse( $translations->find( null, 'warning' ) );
+ }
+
+ public function test_does_not_match_non_component_html() {
+ $translations = $this->extract(
+ '{{ __(\'Link text\', \'foo-theme\') }}'
+ );
+
+ $this->assertNotFalse( $translations->find( null, 'Link text' ) );
+ // Only 1 translation should exist.
+ $this->assertCount( 1, $translations );
+ }
+
+ public function test_extracts_context_function_in_prop() {
+ $translations = $this->extract(
+ ''
+ );
+
+ $translation = $translations->find( 'verb', 'Read' );
+ $this->assertNotFalse( $translation );
+ }
+
+ public function test_extracts_esc_functions_in_props() {
+ $blade = <<<'BLADE'
+
+BLADE;
+
+ $translations = $this->extract( $blade );
+
+ $this->assertNotFalse( $translations->find( null, 'Username' ) );
+ $this->assertNotFalse( $translations->find( null, 'Enter username' ) );
+ }
+
+ public function test_extracts_single_quoted_bound_props() {
+ $translations = $this->extract(
+ ""
+ );
+
+ $this->assertNotFalse( $translations->find( null, 'Single quoted' ) );
+ }
+
+ public function test_existing_blade_extraction_still_works() {
+ $blade = <<<'BLADE'
+@php
+ __('PHP block string', 'foo-theme');
+@endphp
+{{ __('Echo string', 'foo-theme') }}
+{!! __('Raw string', 'foo-theme') !!}
+@php(__('Directive string', 'foo-theme'))
+BLADE;
+
+ $translations = $this->extract( $blade );
+
+ $this->assertNotFalse( $translations->find( null, 'PHP block string' ) );
+ $this->assertNotFalse( $translations->find( null, 'Echo string' ) );
+ $this->assertNotFalse( $translations->find( null, 'Raw string' ) );
+ $this->assertNotFalse( $translations->find( null, 'Directive string' ) );
+ }
+}