From 5e178ff9c91467d6bdce9631cae2176238c7d5c9 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 1 Feb 2024 20:57:03 +0000 Subject: [PATCH] I18N: Fix plural forms parsing in `WP_Translation_File`. Ensures the plural expression from the translation file header is correctly parsed. Prevents silent failures in the attempt to create the plural form function. Adds additional tests. Props Chouby. See #59656. git-svn-id: https://develop.svn.wordpress.org/trunk@57518 602fd350-edb4-49c9-b593-d223f7449a82 --- .../l10n/class-wp-translation-file.php | 25 ++++++++-- tests/phpunit/data/l10n/plural-complex.mo | Bin 0 -> 580 bytes tests/phpunit/data/l10n/plural-complex.php | 2 + tests/phpunit/tests/l10n/wpTranslations.php | 46 ++++++++++++++++++ 4 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 tests/phpunit/data/l10n/plural-complex.mo create mode 100644 tests/phpunit/data/l10n/plural-complex.php diff --git a/src/wp-includes/l10n/class-wp-translation-file.php b/src/wp-includes/l10n/class-wp-translation-file.php index 93842bec10..e6dbdb85be 100644 --- a/src/wp-includes/l10n/class-wp-translation-file.php +++ b/src/wp-includes/l10n/class-wp-translation-file.php @@ -207,7 +207,7 @@ abstract class WP_Translation_File { } /** - * Returns the plural form for a count. + * Returns the plural form for a given number. * * @since 6.5.0 * @@ -219,9 +219,9 @@ abstract class WP_Translation_File { $this->parse_file(); } - // In case a plural form is specified as a header, but no function included, build one. if ( null === $this->plural_forms && isset( $this->headers['plural-forms'] ) ) { - $this->plural_forms = $this->make_plural_form_function( $this->headers['plural-forms'] ); + $expression = $this->get_plural_expression_from_header( $this->headers['plural-forms'] ); + $this->plural_forms = $this->make_plural_form_function( $expression ); } if ( is_callable( $this->plural_forms ) ) { @@ -231,6 +231,7 @@ abstract class WP_Translation_File { * @var int $result Plural form. */ $result = call_user_func( $this->plural_forms, $number ); + return $result; } @@ -238,6 +239,22 @@ abstract class WP_Translation_File { return ( 1 === $number ? 0 : 1 ); } + /** + * Returns the plural forms expression as a tuple. + * + * @since 6.5.0 + * + * @param string $header Plural-Forms header string. + * @return string Plural forms expression. + */ + protected function get_plural_expression_from_header( $header ) { + if ( preg_match( '/^\s*nplurals\s*=\s*(\d+)\s*;\s+plural\s*=\s*(.+)$/', $header, $matches ) ) { + return trim( $matches[2] ); + } + + return 'n != 1'; + } + /** * Makes a function, which will return the right translation index, according to the * plural forms header. @@ -247,7 +264,7 @@ abstract class WP_Translation_File { * @param string $expression Plural form expression. * @return callable(int $num): int Plural forms function. */ - public function make_plural_form_function( string $expression ): callable { + protected function make_plural_form_function( string $expression ): callable { try { $handler = new Plural_Forms( rtrim( $expression, ';' ) ); return array( $handler, 'get' ); diff --git a/tests/phpunit/data/l10n/plural-complex.mo b/tests/phpunit/data/l10n/plural-complex.mo new file mode 100644 index 0000000000000000000000000000000000000000..9cb7b2820e580b87fd5c3413afb77416d3100d17 GIT binary patch literal 580 zcmZWmJx{|h5G{x)BQwJ-NUgxB6H-*AL=4cPN+2q}H{>=)F*0^!yHP>>4gL-r6aRtV z!lfNj;H1+#zxVE>JAZF(yf-M;DYqzhs|Urw5*FB`I-|_j8jT0SIpr7Cjco_BA{QP( zltL;oR#ntCxZrJ)^v4OGA zE(KjIo-Rvzuq?BthHZ#Ahb)7r1&6>Ph2|<7S8Vi`-GjF`Sm{kFS4^(GR;w4>u@ls2 zg*P^Z6J`9!qI2ye;;@L2-PjPE(zGwvo`(YB! z36HTvWr}AyPf%Unir2y@^ZB8kWGSF9&g6!6$MT;lYqgaBR9sOPfBI{(e5_>}{R7Oj BtMUK< literal 0 HcmV?d00001 diff --git a/tests/phpunit/data/l10n/plural-complex.php b/tests/phpunit/data/l10n/plural-complex.php new file mode 100644 index 0000000000..b214214f2a --- /dev/null +++ b/tests/phpunit/data/l10n/plural-complex.php @@ -0,0 +1,2 @@ +'GlotPress/4.0.0-beta.2','translation-revision-date'=>'2024-01-18 05:40:05+0000','plural-forms'=>'nplurals=4; plural=(n % 100 == 1) ? 0 : ((n % 100 == 2) ? 1 : ((n % 100 == 3 || n % 100 == 4) ? 2 : 3));','project-id-version'=>'WordPress - 6.4.x - Development','language'=>'sl_SI','messages'=>['%s update available'=>'%s razpoložljiva posodobitev' . "\0" . '%s razpoložljivi posodobitvi' . "\0" . '%s razpoložljive posodobitve' . "\0" . '%s razpoložljivih posodobitev']]; \ No newline at end of file diff --git a/tests/phpunit/tests/l10n/wpTranslations.php b/tests/phpunit/tests/l10n/wpTranslations.php index 3879a570e3..70abc4fd30 100644 --- a/tests/phpunit/tests/l10n/wpTranslations.php +++ b/tests/phpunit/tests/l10n/wpTranslations.php @@ -219,6 +219,52 @@ class WP_Translations_Tests extends WP_UnitTestCase { $this->assertTrue( $unload_successful, 'Text domain not successfully unloaded' ); } + /** + * @covers ::translate_plural + * @covers WP_Translation_File::get_plural_form + */ + public function test_translate_plural_complex() { + load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/l10n/plural-complex.mo' ); + + $this->assertSame( '%s razpoložljiva posodobitev', _n( '%s update available', '%s updates available', 101, 'wp-tests-domain' ) ); // 1, 101, 201 + $this->assertSame( '%s razpoložljivi posodobitvi', _n( '%s update available', '%s updates available', 102, 'wp-tests-domain' ) ); // 2, 102, 202 + $this->assertSame( '%s razpoložljive posodobitve', _n( '%s update available', '%s updates available', 103, 'wp-tests-domain' ) ); // 3, 4, 103 + $this->assertSame( '%s razpoložljivih posodobitev', _n( '%s update available', '%s updates available', 5, 'wp-tests-domain' ) ); // 0, 5, 6 + } + + /** + * @covers ::translate_plural + * @covers WP_Translation_File::get_plural_form + */ + public function test_translate_plural_complex_php() { + load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/l10n/plural-complex.php' ); + + $this->assertSame( '%s razpoložljiva posodobitev', _n( '%s update available', '%s updates available', 101, 'wp-tests-domain' ) ); // 1, 101, 201 + $this->assertSame( '%s razpoložljivi posodobitvi', _n( '%s update available', '%s updates available', 102, 'wp-tests-domain' ) ); // 2, 102, 202 + $this->assertSame( '%s razpoložljive posodobitve', _n( '%s update available', '%s updates available', 103, 'wp-tests-domain' ) ); // 3, 4, 103 + $this->assertSame( '%s razpoložljivih posodobitev', _n( '%s update available', '%s updates available', 5, 'wp-tests-domain' ) ); // 0, 5, 6 + } + + /** + * @covers WP_Translation_File::get_plural_form + */ + public function test_get_plural_form() { + $moe = WP_Translation_File::create( DIR_TESTDATA . '/l10n/plural-complex.mo' ); + + $this->assertSame( 0, $moe->get_plural_form( 1 ) ); + $this->assertSame( 0, $moe->get_plural_form( 101 ) ); + $this->assertSame( 0, $moe->get_plural_form( 201 ) ); + $this->assertSame( 1, $moe->get_plural_form( 2 ) ); + $this->assertSame( 1, $moe->get_plural_form( 102 ) ); + $this->assertSame( 1, $moe->get_plural_form( 202 ) ); + $this->assertSame( 2, $moe->get_plural_form( 3 ) ); + $this->assertSame( 2, $moe->get_plural_form( 4 ) ); + $this->assertSame( 2, $moe->get_plural_form( 103 ) ); + $this->assertSame( 3, $moe->get_plural_form( 0 ) ); + $this->assertSame( 3, $moe->get_plural_form( 5 ) ); + $this->assertSame( 3, $moe->get_plural_form( 6 ) ); + } + /** * @covers ::translate_plural */