<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[57337] trunk: I18N: Introduce a more performant localization library.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/57337">57337</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/57337","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>swissspidy</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2024-01-23 13:32:34 +0000 (Tue, 23 Jan 2024)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>I18N: Introduce a more performant localization library.

This introduces a more lightweight library for loading `.mo` translation files which offers increased speed and lower memory usage.
It also supports loading multiple locales at the same time, which makes locale switching faster too.

For plugins interacting with the `$l10n` global variable in core, a shim is added to retain backward compatibility with the existing `pomo` library.

In addition to that, this library supports translations contained in PHP files, avoiding a binary file format and leveraging OPCache if available.
If an `.mo` translation file has a corresponding `.l10n.php` file, the latter will be loaded instead.
This behavior can be adjusted using the new `translation_file_format` and `load_translation_file` filters.

PHP translation files will be typically created by downloading language packs, but can also be generated by plugins.
See https://make.wordpress.org/core/2023/11/08/merging-performant-translations-into-core/ for more context.

Props dd32, swissspidy, flixos90, joemcgill, westonruter, akirk, SergeyBiryukov.
Fixes <a href="https://core.trac.wordpress.org/ticket/59656">#59656</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpadminincludespluginphp">trunk/src/wp-admin/includes/plugin.php</a></li>
<li><a href="#trunksrcwpincludesclasswplocaleswitcherphp">trunk/src/wp-includes/class-wp-locale-switcher.php</a></li>
<li><a href="#trunksrcwpincludescompatphp">trunk/src/wp-includes/compat.php</a></li>
<li><a href="#trunksrcwpincludesfunctionsphp">trunk/src/wp-includes/functions.php</a></li>
<li><a href="#trunksrcwpincludesl10nphp">trunk/src/wp-includes/l10n.php</a></li>
<li><a href="#trunksrcwpsettingsphp">trunk/src/wp-settings.php</a></li>
<li><a href="#trunktestsphpunittestsl10nloadTextdomainJustInTimephp">trunk/tests/phpunit/tests/l10n/loadTextdomainJustInTime.php</a></li>
<li><a href="#trunktestsphpunittestsl10nwpLocaleSwitcherphp">trunk/tests/phpunit/tests/l10n/wpLocaleSwitcher.php</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li>trunk/src/wp-includes/l10n/</li>
<li><a href="#trunksrcwpincludesl10nclasswptranslationcontrollerphp">trunk/src/wp-includes/l10n/class-wp-translation-controller.php</a></li>
<li><a href="#trunksrcwpincludesl10nclasswptranslationfilemophp">trunk/src/wp-includes/l10n/class-wp-translation-file-mo.php</a></li>
<li><a href="#trunksrcwpincludesl10nclasswptranslationfilephpphp">trunk/src/wp-includes/l10n/class-wp-translation-file-php.php</a></li>
<li><a href="#trunksrcwpincludesl10nclasswptranslationfilephp">trunk/src/wp-includes/l10n/class-wp-translation-file.php</a></li>
<li><a href="#trunksrcwpincludesl10nclasswptranslationsphp">trunk/src/wp-includes/l10n/class-wp-translations.php</a></li>
<li>trunk/tests/phpunit/data/l10n/</li>
<li><a href="#trunktestsphpunitdatal10nexamplesimplemo">trunk/tests/phpunit/data/l10n/example-simple.mo</a></li>
<li><a href="#trunktestsphpunitdatal10nexamplesimplephp">trunk/tests/phpunit/data/l10n/example-simple.php</a></li>
<li><a href="#trunktestsphpunitdatal10nexamplesimplepo">trunk/tests/phpunit/data/l10n/example-simple.po</a></li>
<li><a href="#trunktestsphpunitdatal10nfa_IRmo">trunk/tests/phpunit/data/l10n/fa_IR.mo</a></li>
<li><a href="#trunktestsphpunitdatal10npluralmo">trunk/tests/phpunit/data/l10n/plural.mo</a></li>
<li><a href="#trunktestsphpunitdatal10nsimplemo">trunk/tests/phpunit/data/l10n/simple.mo</a></li>
<li><a href="#trunktestsphpunitdatapomosimplel10nphp">trunk/tests/phpunit/data/pomo/simple.l10n.php</a></li>
<li><a href="#trunktestsphpunittestsl10nwpTranslationControllerphp">trunk/tests/phpunit/tests/l10n/wpTranslationController.php</a></li>
<li><a href="#trunktestsphpunittestsl10nwpTranslationsphp">trunk/tests/phpunit/tests/l10n/wpTranslations.php</a></li>
<li><a href="#trunktestsphpunittestsl10nwpTranslationsConvertphp">trunk/tests/phpunit/tests/l10n/wpTranslationsConvert.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpadminincludespluginphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/includes/plugin.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/includes/plugin.php    2024-01-23 13:05:08 UTC (rev 57336)
+++ trunk/src/wp-admin/includes/plugin.php      2024-01-23 13:32:34 UTC (rev 57337)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1009,6 +1009,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        foreach ( $translations as $translation => $data ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                $wp_filesystem->delete( WP_LANG_DIR . '/plugins/' . $plugin_slug . '-' . $translation . '.po' );
</span><span class="cx" style="display: block; padding: 0 10px">                                $wp_filesystem->delete( WP_LANG_DIR . '/plugins/' . $plugin_slug . '-' . $translation . '.mo' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                $wp_filesystem->delete( WP_LANG_DIR . '/plugins/' . $plugin_slug . '-' . $translation . '.l10n.php' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                $json_translation_files = glob( WP_LANG_DIR . '/plugins/' . $plugin_slug . '-' . $translation . '-*.json' );
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( $json_translation_files ) {
</span></span></pre></div>
<a id="trunksrcwpincludesclasswplocaleswitcherphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-locale-switcher.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-locale-switcher.php        2024-01-23 13:05:08 UTC (rev 57336)
+++ trunk/src/wp-includes/class-wp-locale-switcher.php  2024-01-23 13:32:34 UTC (rev 57337)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -283,6 +283,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $wp_locale = new WP_Locale();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                WP_Translation_Controller::instance()->set_locale( $locale );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 /**
</span><span class="cx" style="display: block; padding: 0 10px">                 * Fires when the locale is switched to or restored.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span></span></pre></div>
<a id="trunksrcwpincludescompatphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/compat.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/compat.php  2024-01-23 13:05:08 UTC (rev 57336)
+++ trunk/src/wp-includes/compat.php    2024-01-23 13:32:34 UTC (rev 57337)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -420,6 +420,38 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+if ( ! function_exists( 'array_is_list' ) ) {
+       /**
+        * Polyfill for `array_is_list()` function added in PHP 8.1.
+        *
+        * Determines if the given array is a list.
+        *
+        * An array is considered a list if its keys consist of consecutive numbers from 0 to count($array)-1.
+        *
+        * @see https://github.com/symfony/polyfill-php81/tree/main
+        *
+        * @since 6.5.0
+        *
+        * @param array<mixed> $arr The array being evaluated.
+        * @return bool True if array is a list, false otherwise.
+        */
+       function array_is_list( $arr ) {
+               if ( ( array() === $arr ) || ( array_values( $arr ) === $arr ) ) {
+                       return true;
+               }
+
+               $next_key = -1;
+
+               foreach ( $arr as $k => $v ) {
+                       if ( ++$next_key !== $k ) {
+                               return false;
+                       }
+               }
+
+               return true;
+       }
+}
+
</ins><span class="cx" style="display: block; padding: 0 10px"> if ( ! function_exists( 'str_contains' ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="cx" style="display: block; padding: 0 10px">         * Polyfill for `str_contains()` function added in PHP 8.0.
</span></span></pre></div>
<a id="trunksrcwpincludesfunctionsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/functions.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/functions.php       2024-01-23 13:05:08 UTC (rev 57336)
+++ trunk/src/wp-includes/functions.php 2024-01-23 13:32:34 UTC (rev 57337)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -6550,7 +6550,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        if ( ! $mo_loaded || $locale !== $locale_loaded ) {
</span><span class="cx" style="display: block; padding: 0 10px">                $locale_loaded = $locale ? $locale : get_locale();
</span><span class="cx" style="display: block; padding: 0 10px">                $mofile        = WP_LANG_DIR . '/continents-cities-' . $locale_loaded . '.mo';
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                unload_textdomain( 'continents-cities' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         unload_textdomain( 'continents-cities', true );
</ins><span class="cx" style="display: block; padding: 0 10px">                 load_textdomain( 'continents-cities', $mofile, $locale_loaded );
</span><span class="cx" style="display: block; padding: 0 10px">                $mo_loaded = true;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span></span></pre></div>
<a id="trunksrcwpincludesl10nclasswptranslationcontrollerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/l10n/class-wp-translation-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/l10n/class-wp-translation-controller.php                            (rev 0)
+++ trunk/src/wp-includes/l10n/class-wp-translation-controller.php      2024-01-23 13:32:34 UTC (rev 57337)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,420 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * I18N: WP_Translation_Controller class.
+ *
+ * @package WordPress
+ * @subpackage I18N
+ * @since 6.5.0
+ */
+
+/**
+ * Class WP_Translation_Controller.
+ *
+ * @since 6.5.0
+ */
+final class WP_Translation_Controller {
+       /**
+        * Current locale.
+        *
+        * @since 6.5.0
+        * @var string
+        */
+       protected $current_locale = 'en_US';
+
+       /**
+        * Map of loaded translations per locale and text domain.
+        *
+        * [ Locale => [ Textdomain => [ ..., ... ] ] ]
+        *
+        * @since 6.5.0
+        * @var array<string, array<string, WP_Translation_File[]>>
+        */
+       protected $loaded_translations = array();
+
+       /**
+        * List of loaded translation files.
+        *
+        * [ Filename => [ Locale => [ Textdomain => WP_Translation_File ] ] ]
+        *
+        * @since 6.5.0
+        * @var array<string, array<string, array<string, WP_Translation_File|false>>>
+        */
+       protected $loaded_files = array();
+
+       /**
+        * Returns the WP_Translation_Controller singleton.
+        *
+        * @since 6.5.0
+        *
+        * @return WP_Translation_Controller
+        */
+       public static function instance(): WP_Translation_Controller {
+               static $instance;
+
+               if ( ! $instance ) {
+                       $instance = new self();
+               }
+
+               return $instance;
+       }
+
+       /**
+        * Returns the current locale.
+        *
+        * @since 6.5.0
+        *
+        * @return string Locale.
+        */
+       public function get_locale(): string {
+               return $this->current_locale;
+       }
+
+       /**
+        * Sets the current locale.
+        *
+        * @since 6.5.0
+        *
+        * @param string $locale Locale.
+        */
+       public function set_locale( string $locale ) {
+               $this->current_locale = $locale;
+       }
+
+       /**
+        * Loads a translation file for a given text domain.
+        *
+        * @since 6.5.0
+        *
+        * @param string $translation_file Translation file.
+        * @param string $textdomain       Optional. Text domain. Default 'default'.
+        * @param string $locale           Optional. Locale. Default current locale.
+        * @return bool True on success, false otherwise.
+        */
+       public function load_file( string $translation_file, string $textdomain = 'default', string $locale = null ): bool {
+               if ( null === $locale ) {
+                       $locale = $this->current_locale;
+               }
+
+               $translation_file = realpath( $translation_file );
+
+               if ( false === $translation_file ) {
+                       return false;
+               }
+
+               if (
+                       isset( $this->loaded_files[ $translation_file ][ $locale ][ $textdomain ] ) &&
+                       false !== $this->loaded_files[ $translation_file ][ $locale ][ $textdomain ]
+               ) {
+                       return null === $this->loaded_files[ $translation_file ][ $locale ][ $textdomain ]->error();
+               }
+
+               if (
+                       isset( $this->loaded_files[ $translation_file ][ $locale ] ) &&
+                       array() !== $this->loaded_files[ $translation_file ][ $locale ]
+               ) {
+                       $moe = reset( $this->loaded_files[ $translation_file ][ $locale ] );
+               } else {
+                       $moe = WP_Translation_File::create( $translation_file );
+                       if ( false === $moe || null !== $moe->error() ) {
+                               $moe = false;
+                       }
+               }
+
+               $this->loaded_files[ $translation_file ][ $locale ][ $textdomain ] = $moe;
+
+               if ( ! $moe instanceof WP_Translation_File ) {
+                       return false;
+               }
+
+               if ( ! isset( $this->loaded_translations[ $locale ][ $textdomain ] ) ) {
+                       $this->loaded_translations[ $locale ][ $textdomain ] = array();
+               }
+
+               $this->loaded_translations[ $locale ][ $textdomain ][] = $moe;
+
+               return true;
+       }
+
+       /**
+        * Unloads a translation file for a given text domain.
+        *
+        * @since 6.5.0
+        *
+        * @param WP_Translation_File|string $file       Translation file instance or file name.
+        * @param string                     $textdomain Optional. Text domain. Default 'default'.
+        * @param string                     $locale     Optional. Locale. Defaults to all locales.
+        * @return bool True on success, false otherwise.
+        */
+       public function unload_file( $file, string $textdomain = 'default', string $locale = null ): bool {
+               if ( is_string( $file ) ) {
+                       $file = realpath( $file );
+               }
+
+               if ( null !== $locale ) {
+                       foreach ( $this->loaded_translations[ $locale ][ $textdomain ] as $i => $moe ) {
+                               if ( $file === $moe || $file === $moe->get_file() ) {
+                                       unset( $this->loaded_translations[ $locale ][ $textdomain ][ $i ] );
+                                       unset( $this->loaded_files[ $moe->get_file() ][ $locale ][ $textdomain ] );
+                                       return true;
+                               }
+                       }
+
+                       return true;
+               }
+
+               foreach ( $this->loaded_translations as $l => $domains ) {
+                       foreach ( $domains[ $textdomain ] as $i => $moe ) {
+                               if ( $file === $moe || $file === $moe->get_file() ) {
+                                       unset( $this->loaded_translations[ $l ][ $textdomain ][ $i ] );
+                                       unset( $this->loaded_files[ $moe->get_file() ][ $l ][ $textdomain ] );
+                                       return true;
+                               }
+                       }
+               }
+
+               return false;
+       }
+
+       /**
+        * Unloads all translation files for a given text domain.
+        *
+        * @since 6.5.0
+        *
+        * @param string $textdomain Optional. Text domain. Default 'default'.
+        * @param string $locale     Optional. Locale. Defaults to all locales.
+        * @return bool True on success, false otherwise.
+        */
+       public function unload_textdomain( string $textdomain = 'default', string $locale = null ): bool {
+               if ( null !== $locale ) {
+                       foreach ( $this->loaded_translations[ $locale ][ $textdomain ] as $moe ) {
+                               unset( $this->loaded_files[ $moe->get_file() ][ $locale ][ $textdomain ] );
+                       }
+
+                       unset( $this->loaded_translations[ $locale ][ $textdomain ] );
+
+                       return true;
+               }
+
+               $unloaded = false;
+
+               foreach ( $this->loaded_translations as $l => $domains ) {
+                       if ( ! isset( $domains[ $textdomain ] ) ) {
+                               continue;
+                       }
+
+                       $unloaded = true;
+
+                       foreach ( $domains[ $textdomain ] as $moe ) {
+                               unset( $this->loaded_files[ $moe->get_file() ][ $l ][ $textdomain ] );
+                       }
+
+                       unset( $this->loaded_translations[ $l ][ $textdomain ] );
+               }
+
+               return $unloaded;
+       }
+
+       /**
+        * Determines whether translations are loaded for a given text domain.
+        *
+        * @since 6.5.0
+        *
+        * @param string $textdomain Optional. Text domain. Default 'default'.
+        * @param string $locale     Optional. Locale. Default current locale.
+        * @return bool True if there are any loaded translations, false otherwise.
+        */
+       public function is_textdomain_loaded( string $textdomain = 'default', string $locale = null ): bool {
+               if ( null === $locale ) {
+                       $locale = $this->current_locale;
+               }
+
+               return isset( $this->loaded_translations[ $locale ][ $textdomain ] ) &&
+                       array() !== $this->loaded_translations[ $locale ][ $textdomain ];
+       }
+
+       /**
+        * Translates a singular string.
+        *
+        * @since 6.5.0
+        *
+        * @param string $text       Text to translate.
+        * @param string $context    Optional. Context for the string. Default empty string.
+        * @param string $textdomain Optional. Text domain. Default 'default'.
+        * @param string $locale     Optional. Locale. Default current locale.
+        * @return string|false Translation on success, false otherwise.
+        */
+       public function translate( string $text, string $context = '', string $textdomain = 'default', string $locale = null ) {
+               if ( '' !== $context ) {
+                       $context .= "\4";
+               }
+
+               $translation = $this->locate_translation( "{$context}{$text}", $textdomain, $locale );
+
+               if ( false === $translation ) {
+                       return false;
+               }
+
+               return $translation['entries'][0];
+       }
+
+       /**
+        * Translates plurals.
+        *
+        * Checks both singular+plural combinations as well as just singulars,
+        * in case the translation file does not store the plural.
+        *
+        * @since 6.5.0
+        *
+        * @param array{0: string, 1: string} $plurals {
+        *     Pair of singular and plural translations.
+        *
+        *     @type string $0 Singular translation.
+        *     @type string $1 Plural translation.
+        * }
+        * @param int                         $number     Number of items.
+        * @param string                      $context    Optional. Context for the string. Default empty string.
+        * @param string                      $textdomain Optional. Text domain. Default 'default'.
+        * @param string                      $locale     Optional. Locale. Default current locale.
+        * @return string|false Translation on success, false otherwise.
+        */
+       public function translate_plural( array $plurals, int $number, string $context = '', string $textdomain = 'default', string $locale = null ) {
+               if ( '' !== $context ) {
+                       $context .= "\4";
+               }
+
+               $text        = implode( "\0", $plurals );
+               $translation = $this->locate_translation( "{$context}{$text}", $textdomain, $locale );
+
+               if ( false === $translation ) {
+                       $text        = $plurals[0];
+                       $translation = $this->locate_translation( "{$context}{$text}", $textdomain, $locale );
+
+                       if ( false === $translation ) {
+                               return false;
+                       }
+               }
+
+               /** @var WP_Translation_File $source */
+               $source = $translation['source'];
+               $num    = $source->get_plural_form( $number );
+
+               // See \Translations::translate_plural().
+               return $translation['entries'][ $num ] ?? $translation['entries'][0];
+       }
+
+       /**
+        * Returns all existing headers for a given text domain.
+        *
+        * @since 6.5.0
+        *
+        * @param string $textdomain Optional. Text domain. Default 'default'.
+        * @return array<string, string> Headers.
+        */
+       public function get_headers( string $textdomain = 'default' ): array {
+               if ( array() === $this->loaded_translations ) {
+                       return array();
+               }
+
+               $headers = array();
+
+               foreach ( $this->get_files( $textdomain ) as $moe ) {
+                       foreach ( $moe->headers() as $header => $value ) {
+                               $headers[ $this->normalize_header( $header ) ] = $value;
+                       }
+               }
+
+               return $headers;
+       }
+
+       /**
+        * Normalizes header names to be capitalized.
+        *
+        * @since 6.5.0
+        *
+        * @param string $header Header name.
+        * @return string Normalized header name.
+        */
+       protected function normalize_header( string $header ): string {
+               $parts = explode( '-', $header );
+               $parts = array_map( 'ucfirst', $parts );
+               return implode( '-', $parts );
+       }
+
+       /**
+        * Returns all entries for a given text domain.
+        *
+        * @since 6.5.0
+        *
+        * @param string $textdomain Optional. Text domain. Default 'default'.
+        * @return array<string, string> Entries.
+        */
+       public function get_entries( string $textdomain = 'default' ): array {
+               if ( array() === $this->loaded_translations ) {
+                       return array();
+               }
+
+               $entries = array();
+
+               foreach ( $this->get_files( $textdomain ) as $moe ) {
+                       $entries = array_merge( $entries, $moe->entries() );
+               }
+
+               return $entries;
+       }
+
+       /**
+        * Locates translation for a given string and text domain.
+        *
+        * @since 6.5.0
+        *
+        * @param string $singular   Singular translation.
+        * @param string $textdomain Optional. Text domain. Default 'default'.
+        * @param string $locale     Optional. Locale. Default current locale.
+        * @return array{source: WP_Translation_File, entries: string[]}|false {
+        *     Translations on success, false otherwise.
+        *
+        *     @type WP_Translation_File $source Translation file instance.
+        *     @type string[]            $entries Array of translation entries.
+        * }
+        */
+       protected function locate_translation( string $singular, string $textdomain = 'default', string $locale = null ) {
+               if ( array() === $this->loaded_translations ) {
+                       return false;
+               }
+
+               // Find the translation in all loaded files for this text domain.
+               foreach ( $this->get_files( $textdomain, $locale ) as $moe ) {
+                       $translation = $moe->translate( $singular );
+                       if ( false !== $translation ) {
+                               return array(
+                                       'entries' => explode( "\0", $translation ),
+                                       'source'  => $moe,
+                               );
+                       }
+                       if ( null !== $moe->error() ) {
+                               // Unload this file, something is wrong.
+                               $this->unload_file( $moe, $textdomain, $locale );
+                       }
+               }
+
+               // Nothing could be found.
+               return false;
+       }
+
+       /**
+        * Returns all translation files for a given text domain.
+        *
+        * @since 6.5.0
+        *
+        * @param string $textdomain Optional. Text domain. Default 'default'.
+        * @param string $locale     Optional. Locale. Default current locale.
+        * @return WP_Translation_File[] List of translation files.
+        */
+       protected function get_files( string $textdomain = 'default', string $locale = null ): array {
+               if ( null === $locale ) {
+                       $locale = $this->current_locale;
+               }
+
+               return $this->loaded_translations[ $locale ][ $textdomain ] ?? array();
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/src/wp-includes/l10n/class-wp-translation-controller.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunksrcwpincludesl10nclasswptranslationfilemophp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/l10n/class-wp-translation-file-mo.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/l10n/class-wp-translation-file-mo.php                               (rev 0)
+++ trunk/src/wp-includes/l10n/class-wp-translation-file-mo.php 2024-01-23 13:32:34 UTC (rev 57337)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,219 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * I18N: WP_Translation_File_MO class.
+ *
+ * @package WordPress
+ * @subpackage I18N
+ * @since 6.5.0
+ */
+
+/**
+ * Class WP_Translation_File_MO.
+ *
+ * @since 6.5.0
+ */
+class WP_Translation_File_MO extends WP_Translation_File {
+       /**
+        * Endian value.
+        *
+        * V for little endian, N for big endian, or false.
+        *
+        * Used for unpack().
+        *
+        * @since 6.5.0
+        * @var false|'V'|'N'
+        */
+       protected $uint32 = false;
+
+       /**
+        * The magic number of the GNU message catalog format.
+        *
+        * @since 6.5.0
+        * @var int
+        */
+       const MAGIC_MARKER = 0x950412de;
+
+       /**
+        * Detects endian and validates file.
+        *
+        * @since 6.5.0
+        *
+        * @param string $header File contents.
+        * @return false|'V'|'N' V for little endian, N for big endian, or false on failure.
+        */
+       protected function detect_endian_and_validate_file( string $header ) {
+               $big = unpack( 'N', $header );
+
+               if ( false === $big ) {
+                       return false;
+               }
+
+               $big = reset( $big );
+
+               if ( false === $big ) {
+                       return false;
+               }
+
+               $little = unpack( 'V', $header );
+
+               if ( false === $little ) {
+                       return false;
+               }
+
+               $little = reset( $little );
+
+               if ( false === $little ) {
+                       return false;
+               }
+
+               if ( self::MAGIC_MARKER === $big ) {
+                       return 'N';
+               }
+
+               if ( self::MAGIC_MARKER === $little ) {
+                       return 'V';
+               }
+
+               $this->error = 'Magic marker does not exist';
+               return false;
+       }
+
+       /**
+        * Parses the file.
+        *
+        * @since 6.5.0
+        *
+        * @return bool True on success, false otherwise.
+        */
+       protected function parse_file(): bool {
+               $this->parsed = true;
+
+               $file_contents = file_get_contents( $this->file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
+
+               if ( false === $file_contents ) {
+                       return false;
+               }
+
+               $file_length = strlen( $file_contents );
+
+               if ( $file_length < 24 ) {
+                       $this->error = 'Invalid data';
+                       return false;
+               }
+
+               $this->uint32 = $this->detect_endian_and_validate_file( substr( $file_contents, 0, 4 ) );
+
+               if ( false === $this->uint32 ) {
+                       return false;
+               }
+
+               $offsets = substr( $file_contents, 4, 24 );
+
+               if ( false === $offsets ) {
+                       return false;
+               }
+
+               $offsets = unpack( "{$this->uint32}rev/{$this->uint32}total/{$this->uint32}originals_addr/{$this->uint32}translations_addr/{$this->uint32}hash_length/{$this->uint32}hash_addr", $offsets );
+
+               if ( false === $offsets ) {
+                       return false;
+               }
+
+               $offsets['originals_length']    = $offsets['translations_addr'] - $offsets['originals_addr'];
+               $offsets['translations_length'] = $offsets['hash_addr'] - $offsets['translations_addr'];
+
+               if ( $offsets['rev'] > 0 ) {
+                       $this->error = 'Unsupported revision';
+                       return false;
+               }
+
+               if ( $offsets['translations_addr'] > $file_length || $offsets['originals_addr'] > $file_length ) {
+                       $this->error = 'Invalid data';
+                       return false;
+               }
+
+               // Load the Originals.
+               $original_data     = str_split( substr( $file_contents, $offsets['originals_addr'], $offsets['originals_length'] ), 8 );
+               $translations_data = str_split( substr( $file_contents, $offsets['translations_addr'], $offsets['translations_length'] ), 8 );
+
+               foreach ( array_keys( $original_data ) as $i ) {
+                       $o = unpack( "{$this->uint32}length/{$this->uint32}pos", $original_data[ $i ] );
+                       $t = unpack( "{$this->uint32}length/{$this->uint32}pos", $translations_data[ $i ] );
+
+                       if ( false === $o || false === $t ) {
+                               continue;
+                       }
+
+                       $original    = substr( $file_contents, $o['pos'], $o['length'] );
+                       $translation = substr( $file_contents, $t['pos'], $t['length'] );
+                       // GlotPress bug.
+                       $translation = rtrim( $translation, "\0" );
+
+                       // Metadata about the MO file is stored in the first translation entry.
+                       if ( '' === $original ) {
+                               foreach ( explode( "\n", $translation ) as $meta_line ) {
+                                       if ( '' === $meta_line ) {
+                                               continue;
+                                       }
+
+                                       list( $name, $value ) = array_map( 'trim', explode( ':', $meta_line, 2 ) );
+
+                                       $this->headers[ strtolower( $name ) ] = $value;
+                               }
+                       } else {
+                               $this->entries[ (string) $original ] = $translation;
+                       }
+               }
+
+               return true;
+       }
+
+       /**
+        * Exports translation contents as a string.
+        *
+        * @since 6.5.0
+        *
+        * @return string Translation file contents.
+        */
+       public function export(): string {
+               // Prefix the headers as the first key.
+               $headers_string = '';
+               foreach ( $this->headers as $header => $value ) {
+                       $headers_string .= "{$header}: $value\n";
+               }
+               $entries     = array_merge( array( '' => $headers_string ), $this->entries );
+               $entry_count = count( $entries );
+
+               if ( false === $this->uint32 ) {
+                       $this->uint32 = 'V';
+               }
+
+               $bytes_for_entries = $entry_count * 4 * 2;
+               // Pair of 32bit ints per entry.
+               $originals_addr    = 28; /* header */
+               $translations_addr = $originals_addr + $bytes_for_entries;
+               $hash_addr         = $translations_addr + $bytes_for_entries;
+               $entry_offsets     = $hash_addr;
+
+               $file_header = pack( $this->uint32 . '*', self::MAGIC_MARKER, 0 /* rev */, $entry_count, $originals_addr, $translations_addr, 0 /* hash_length */, $hash_addr );
+
+               $o_entries = '';
+               $t_entries = '';
+               $o_addr    = '';
+               $t_addr    = '';
+
+               foreach ( array_keys( $entries ) as $original ) {
+                       $o_addr        .= pack( $this->uint32 . '*', strlen( $original ), $entry_offsets );
+                       $entry_offsets += strlen( $original ) + 1;
+                       $o_entries     .= $original . "\0";
+               }
+
+               foreach ( $entries as $translations ) {
+                       $t_addr        .= pack( $this->uint32 . '*', strlen( $translations ), $entry_offsets );
+                       $entry_offsets += strlen( $translations ) + 1;
+                       $t_entries     .= $translations . "\0";
+               }
+
+               return $file_header . $o_addr . $t_addr . $o_entries . $t_entries;
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/src/wp-includes/l10n/class-wp-translation-file-mo.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunksrcwpincludesl10nclasswptranslationfilephpphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/l10n/class-wp-translation-file-php.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/l10n/class-wp-translation-file-php.php                              (rev 0)
+++ trunk/src/wp-includes/l10n/class-wp-translation-file-php.php        2024-01-23 13:32:34 UTC (rev 57337)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,83 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * I18N: WP_Translation_File_PHP class.
+ *
+ * @package WordPress
+ * @subpackage I18N
+ * @since 6.5.0
+ */
+
+/**
+ * Class WP_Translation_File_PHP.
+ *
+ * @since 6.5.0
+ */
+class WP_Translation_File_PHP extends WP_Translation_File {
+       /**
+        * Parses the file.
+        *
+        * @since 6.5.0
+        */
+       protected function parse_file() {
+               $this->parsed = true;
+
+               $result = include $this->file;
+               if ( ! $result || ! is_array( $result ) ) {
+                       $this->error = 'Invalid data';
+                       return;
+               }
+
+               if ( isset( $result['messages'] ) && is_array( $result['messages'] ) ) {
+                       foreach ( $result['messages'] as $singular => $translations ) {
+                               if ( is_array( $translations ) ) {
+                                       $this->entries[ $singular ] = implode( "\0", $translations );
+                               } elseif ( is_string( $translations ) ) {
+                                       $this->entries[ $singular ] = $translations;
+                               }
+                       }
+                       unset( $result['messages'] );
+               }
+
+               $this->headers = array_change_key_case( $result );
+       }
+
+       /**
+        * Exports translation contents as a string.
+        *
+        * @since 6.5.0
+        *
+        * @return string Translation file contents.
+        */
+       public function export(): string {
+               $data = array_merge( $this->headers, array( 'messages' => $this->entries ) );
+
+               return '<?php' . PHP_EOL . 'return ' . $this->var_export( $data ) . ';' . PHP_EOL;
+       }
+
+       /**
+        * Outputs or returns a parsable string representation of a variable.
+        *
+        * Like {@see var_export()} but "minified", using short array syntax
+        * and no newlines.
+        *
+        * @since 6.5.0
+        *
+        * @param mixed $value The variable you want to export.
+        * @return string The variable representation.
+        */
+       private function var_export( $value ): string {
+               if ( ! is_array( $value ) ) {
+                       return var_export( $value, true );
+               }
+
+               $entries = array();
+
+               $is_list = array_is_list( $value );
+
+               foreach ( $value as $key => $val ) {
+                       $entries[] = $is_list ? $this->var_export( $val ) : var_export( $key, true ) . '=>' . $this->var_export( $val );
+               }
+
+               return '[' . implode( ',', $entries ) . ']';
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/src/wp-includes/l10n/class-wp-translation-file-php.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunksrcwpincludesl10nclasswptranslationfilephp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/l10n/class-wp-translation-file.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/l10n/class-wp-translation-file.php                          (rev 0)
+++ trunk/src/wp-includes/l10n/class-wp-translation-file.php    2024-01-23 13:32:34 UTC (rev 57337)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,296 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * I18N: WP_Translation_File class.
+ *
+ * @package WordPress
+ * @subpackage I18N
+ * @since 6.5.0
+ */
+
+/**
+ * Class WP_Translation_File.
+ *
+ * @since 6.5.0
+ */
+abstract class WP_Translation_File {
+       /**
+        * List of headers.
+        *
+        * @since 6.5.0
+        * @var array<string, string>
+        */
+       protected $headers = array();
+
+       /**
+        * Whether file has been parsed.
+        *
+        * @since 6.5.0
+        * @var bool
+        */
+       protected $parsed = false;
+
+       /**
+        * Error information.
+        *
+        * @since 6.5.0
+        * @var string|null Error message or null if no error.
+        */
+       protected $error;
+
+       /**
+        * File name.
+        *
+        * @since 6.5.0
+        * @var string
+        */
+       protected $file = '';
+
+       /**
+        * Translation entries.
+        *
+        * @since 6.5.0
+        * @var array<string, string>
+        */
+       protected $entries = array();
+
+       /**
+        * Plural forms function.
+        *
+        * @since 6.5.0
+        * @var callable|null Plural forms.
+        */
+       protected $plural_forms = null;
+
+       /**
+        * Constructor.
+        *
+        * @since 6.5.0
+        *
+        * @param string $file File to load.
+        */
+       protected function __construct( string $file ) {
+               $this->file = $file;
+       }
+
+       /**
+        * Creates a new WP_Translation_File instance for a given file.
+        *
+        * @since 6.5.0
+        *
+        * @param string      $file     File name.
+        * @param string|null $filetype Optional. File type. Default inferred from file name.
+        * @return false|WP_Translation_File
+        */
+       public static function create( string $file, string $filetype = null ) {
+               if ( ! is_readable( $file ) ) {
+                       return false;
+               }
+
+               if ( null === $filetype ) {
+                       $pos = strrpos( $file, '.' );
+                       if ( false !== $pos ) {
+                               $filetype = substr( $file, $pos + 1 );
+                       }
+               }
+
+               switch ( $filetype ) {
+                       case 'mo':
+                               return new WP_Translation_File_MO( $file );
+                       case 'php':
+                               return new WP_Translation_File_PHP( $file );
+                       default:
+                               return false;
+               }
+       }
+
+       /**
+        * Creates a new WP_Translation_File instance for a given file.
+        *
+        * @since 6.5.0
+        *
+        * @param string $file     Source file name.
+        * @param string $filetype Desired target file type.
+        * @return string|false Transformed translation file contents on success, false otherwise.
+        */
+       public static function transform( string $file, string $filetype ) {
+               $source = self::create( $file );
+
+               if ( false === $source ) {
+                       return false;
+               }
+
+               switch ( $filetype ) {
+                       case 'mo':
+                               $destination = new WP_Translation_File_MO( '' );
+                               break;
+                       case 'php':
+                               $destination = new WP_Translation_File_PHP( '' );
+                               break;
+                       default:
+                               return false;
+               }
+
+               $success = $destination->import( $source );
+
+               if ( ! $success ) {
+                       return false;
+               }
+
+               return $destination->export();
+       }
+
+       /**
+        * Returns all headers.
+        *
+        * @since 6.5.0
+        *
+        * @return array<string, string> Headers.
+        */
+       public function headers(): array {
+               if ( ! $this->parsed ) {
+                       $this->parse_file();
+               }
+               return $this->headers;
+       }
+
+       /**
+        * Returns all entries.
+        *
+        * @since 6.5.0
+        *
+        * @return array<string, string[]> Entries.
+        */
+       public function entries(): array {
+               if ( ! $this->parsed ) {
+                       $this->parse_file();
+               }
+
+               return $this->entries;
+       }
+
+       /**
+        * Returns the current error information.
+        *
+        * @since 6.5.0
+        *
+        * @return string|null Error message or null if no error.
+        */
+       public function error() {
+               return $this->error;
+       }
+
+       /**
+        * Returns the file name.
+        *
+        * @since 6.5.0
+        *
+        * @return string File name.
+        */
+       public function get_file(): string {
+               return $this->file;
+       }
+
+       /**
+        * Translates a given string.
+        *
+        * @since 6.5.0
+        *
+        * @param string $text String to translate.
+        * @return false|string Translation(s) on success, false otherwise.
+        */
+       public function translate( string $text ) {
+               if ( ! $this->parsed ) {
+                       $this->parse_file();
+               }
+
+               return $this->entries[ $text ] ?? false;
+       }
+
+       /**
+        * Returns the plural form for a count.
+        *
+        * @since 6.5.0
+        *
+        * @param int $number Count.
+        * @return int Plural form.
+        */
+       public function get_plural_form( int $number ): int {
+               if ( ! $this->parsed ) {
+                       $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'] );
+               }
+
+               if ( is_callable( $this->plural_forms ) ) {
+                       /**
+                        * Plural form.
+                        *
+                        * @var int $result Plural form.
+                        */
+                       $result = call_user_func( $this->plural_forms, $number );
+                       return $result;
+               }
+
+               // Default plural form matches English, only "One" is considered singular.
+               return ( 1 === $number ? 0 : 1 );
+       }
+
+       /**
+        * Makes a function, which will return the right translation index, according to the
+        * plural forms header.
+        *
+        * @since 6.5.0
+        *
+        * @param string $expression Plural form expression.
+        * @return callable(int $num): int Plural forms function.
+        */
+       public function make_plural_form_function( string $expression ): callable {
+               try {
+                       $handler = new Plural_Forms( rtrim( $expression, ';' ) );
+                       return array( $handler, 'get' );
+               } catch ( Exception $e ) {
+                       // Fall back to default plural-form function.
+                       return $this->make_plural_form_function( 'n != 1' );
+               }
+       }
+
+       /**
+        * Imports translations from another file.
+        *
+        * @since 6.5.0
+        *
+        * @param WP_Translation_File $source Source file.
+        * @return bool True on success, false otherwise.
+        */
+       protected function import( WP_Translation_File $source ): bool {
+               if ( null !== $source->error() ) {
+                       return false;
+               }
+
+               $this->headers = $source->headers();
+               $this->entries = $source->entries();
+               $this->error   = $source->error();
+
+               return null === $this->error;
+       }
+
+       /**
+        * Parses the file.
+        *
+        * @since 6.5.0
+        */
+       abstract protected function parse_file();
+
+
+       /**
+        * Exports translation contents as a string.
+        *
+        * @since 6.5.0
+        *
+        * @return string Translation file contents.
+        */
+       abstract public function export();
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/src/wp-includes/l10n/class-wp-translation-file.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunksrcwpincludesl10nclasswptranslationsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/l10n/class-wp-translations.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/l10n/class-wp-translations.php                              (rev 0)
+++ trunk/src/wp-includes/l10n/class-wp-translations.php        2024-01-23 13:32:34 UTC (rev 57337)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,157 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * I18N: WP_Translations class.
+ *
+ * @package WordPress
+ * @subpackage I18N
+ * @since 6.5.0
+ */
+
+/**
+ * Class WP_Translations.
+ *
+ * @since 6.5.0
+ *
+ * @property-read array<string, string> $headers
+ * @property-read array<string, string[]> $entries
+ */
+class WP_Translations {
+       /**
+        * Text domain.
+        *
+        * @since 6.5.0
+        * @var string
+        */
+       protected $textdomain = 'default';
+
+       /**
+        * Translation controller instance.
+        *
+        * @since 6.5.0
+        * @var WP_Translation_Controller
+        */
+       protected $controller;
+
+       /**
+        * Constructor.
+        *
+        * @since 6.5.0
+        *
+        * @param WP_Translation_Controller $controller I18N controller.
+        * @param string                    $textdomain Optional. Text domain. Default 'default'.
+        */
+       public function __construct( WP_Translation_Controller $controller, string $textdomain = 'default' ) {
+               $this->controller = $controller;
+               $this->textdomain = $textdomain;
+       }
+
+       /**
+        * Magic getter for backward compatibility.
+        *
+        * @since 6.5.0
+        *
+        * @param string $name Property name.
+        * @return mixed
+        */
+       public function __get( string $name ) {
+               if ( 'entries' === $name ) {
+                       $entries = $this->controller->get_entries( $this->textdomain );
+
+                       $result = array();
+
+                       foreach ( $entries as $original => $translations ) {
+                               $result[] = $this->make_entry( $original, $translations );
+                       }
+
+                       return $result;
+               }
+
+               if ( 'headers' === $name ) {
+                       return $this->controller->get_headers( $this->textdomain );
+               }
+
+               return null;
+       }
+
+       /**
+        * Builds a Translation_Entry from original string and translation strings.
+        *
+        * @see MO::make_entry()
+        *
+        * @since 6.5.0
+        *
+        * @param string $original    Original string to translate from MO file. Might contain
+        *                            0x04 as context separator or 0x00 as singular/plural separator.
+        * @param string $translations Translation strings from MO file.
+        * @return Translation_Entry Entry instance.
+        */
+       private function make_entry( $original, $translations ): Translation_Entry {
+               $entry = new Translation_Entry();
+
+               // Look for context, separated by \4.
+               $parts = explode( "\4", $original );
+               if ( isset( $parts[1] ) ) {
+                       $original       = $parts[1];
+                       $entry->context = $parts[0];
+               }
+
+               // Look for plural original.
+               $parts           = explode( "\0", $original );
+               $entry->singular = $parts[0];
+               if ( isset( $parts[1] ) ) {
+                       $entry->is_plural = true;
+                       $entry->plural    = $parts[1];
+               }
+
+               $entry->translations = explode( "\0", $translations );
+               return $entry;
+       }
+
+       /**
+        * Translates a plural string.
+        *
+        * @since 6.5.0
+        *
+        * @param string|null $singular Singular string.
+        * @param string|null $plural   Plural string.
+        * @param int|float   $count    Count. Should be an integer, but some plugins pass floats.
+        * @param string|null $context  Context.
+        * @return string|null Translation if it exists, or the unchanged singular string.
+        */
+       public function translate_plural( $singular, $plural, $count = 1, $context = '' ) {
+               if ( null === $singular || null === $plural ) {
+                       return $singular;
+               }
+
+               $translation = $this->controller->translate_plural( array( $singular, $plural ), (int) $count, (string) $context, $this->textdomain );
+               if ( false !== $translation ) {
+                       return $translation;
+               }
+
+               // Fall back to the original with English grammar rules.
+               return ( 1 === $count ? $singular : $plural );
+       }
+
+       /**
+        * Translates a singular string.
+        *
+        * @since 6.5.0
+        *
+        * @param string|null $singular Singular string.
+        * @param string|null $context  Context.
+        * @return string|null Translation if it exists, or the unchanged singular string
+        */
+       public function translate( $singular, $context = '' ) {
+               if ( null === $singular ) {
+                       return null;
+               }
+
+               $translation = $this->controller->translate( $singular, (string) $context, $this->textdomain );
+               if ( false !== $translation ) {
+                       return $translation;
+               }
+
+               // Fall back to the original.
+               return $singular;
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/src/wp-includes/l10n/class-wp-translations.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunksrcwpincludesl10nphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/l10n.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/l10n.php    2024-01-23 13:05:08 UTC (rev 57336)
+++ trunk/src/wp-includes/l10n.php      2024-01-23 13:32:34 UTC (rev 57337)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -797,23 +797,66 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $locale = determine_locale();
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $mo = new MO();
-       if ( ! $mo->import_from_file( $mofile ) ) {
-               $wp_textdomain_registry->set( $domain, $locale, false );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $i18n_controller = WP_Translation_Controller::instance();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                return false;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ // Ensures the correct locale is set as the current one, in case it was filtered.
+       $i18n_controller->set_locale( $locale );
+
+       /**
+        * Filters the preferred file format for translation files.
+        *
+        * Can be used to disable the use of PHP files for translations.
+        *
+        * @since 6.5.0
+        *
+        * @param string $preferred_format Preferred file format. Possible values: 'php', 'mo'. Default: 'php'.
+        * @param string $domain           The text domain.
+        */
+       $preferred_format = apply_filters( 'translation_file_format', 'php', $domain );
+       if ( ! in_array( $preferred_format, array( 'php', 'mo' ), true ) ) {
+               $preferred_format = 'php';
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        if ( isset( $l10n[ $domain ] ) ) {
-               $mo->merge_with( $l10n[ $domain ] );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $translation_files = array( $mofile );
+       if ( 'mo' !== $preferred_format ) {
+               array_unshift(
+                       $translation_files,
+                       substr_replace( $mofile, '.l10n.', - strlen( $preferred_format ) )
+               );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        unset( $l10n_unloaded[ $domain ] );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ foreach ( $translation_files as $file ) {
+               /**
+                * Filters the file path for loading translations for the given text domain.
+                *
+                * Similar to the {@see 'load_textdomain_mofile'} filter with the difference that
+                * the file path could be for an MO or PHP file.
+                *
+                * @since 6.5.0
+                *
+                * @param string $file   Path to the translation file to load.
+                * @param string $domain The text domain.
+                */
+               $file = (string) apply_filters( 'load_translation_file', $file, $domain );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $l10n[ $domain ] = &$mo;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $success = $i18n_controller->load_file( $file, $domain, $locale );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $wp_textdomain_registry->set( $domain, $locale, dirname( $mofile ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( $success ) {
+                       if ( isset( $l10n[ $domain ] ) && $l10n[ $domain ] instanceof MO ) {
+                               $i18n_controller->load_file( $l10n[ $domain ]->get_filename(), $domain, $locale );
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        // Unset NOOP_Translations reference in get_translations_for_domain().
+                       unset( $l10n[ $domain ] );
+
+                       $l10n[ $domain ] = new WP_Translations( $i18n_controller, $domain );
+
+                       $wp_textdomain_registry->set( $domain, $locale, dirname( $file ) );
+
+                       return true;
+               }
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         return true;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -866,6 +909,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        do_action( 'unload_textdomain', $domain, $reloadable );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        // Since multiple locales are supported, reloadable text domains don't actually need to be unloaded.
+       if ( ! $reloadable ) {
+               WP_Translation_Controller::instance()->unload_textdomain( $domain );
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         if ( isset( $l10n[ $domain ] ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                if ( $l10n[ $domain ] instanceof NOOP_Translations ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        unset( $l10n[ $domain ] );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -904,7 +952,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        // Unload previously loaded strings so we can switch translations.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        unload_textdomain( 'default' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ unload_textdomain( 'default', true );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        $return = load_textdomain( 'default', WP_LANG_DIR . "/$locale.mo", $locale );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunksrcwpsettingsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-settings.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-settings.php 2024-01-23 13:05:08 UTC (rev 57336)
+++ trunk/src/wp-settings.php   2024-01-23 13:32:34 UTC (rev 57337)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -115,6 +115,11 @@
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/class-wp.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/class-wp-error.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/pomo/mo.php';
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+require ABSPATH . WPINC . '/l10n/class-wp-translation-controller.php';
+require ABSPATH . WPINC . '/l10n/class-wp-translations.php';
+require ABSPATH . WPINC . '/l10n/class-wp-translation-file.php';
+require ABSPATH . WPINC . '/l10n/class-wp-translation-file-mo.php';
+require ABSPATH . WPINC . '/l10n/class-wp-translation-file-php.php';
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 0.71
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -617,6 +622,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $GLOBALS['wp_locale_switcher'] = new WP_Locale_Switcher();
</span><span class="cx" style="display: block; padding: 0 10px"> $GLOBALS['wp_locale_switcher']->init();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+WP_Translation_Controller::instance()->set_locale( $locale );
+
</ins><span class="cx" style="display: block; padding: 0 10px"> // Load the functions for the active theme, for both parent and child theme if applicable.
</span><span class="cx" style="display: block; padding: 0 10px"> foreach ( wp_get_active_and_valid_themes() as $theme ) {
</span><span class="cx" style="display: block; padding: 0 10px">        if ( file_exists( $theme . '/functions.php' ) ) {
</span></span></pre></div>
<a id="trunktestsphpunitdatal10nexamplesimplemo"></a>
<div class="binary"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/data/l10n/example-simple.mo</h4>
<pre class="diff"><span>
<span class="cx">(Binary files differ)
</span></span></pre></div>
<span class="cx" style="display: block; padding: 0 10px">Index: trunk/tests/phpunit/data/l10n/example-simple.mo
</span><span class="cx" style="display: block; padding: 0 10px">===================================================================
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">--- trunk/tests/phpunit/data/l10n/example-simple.mo      2024-01-23 13:05:08 UTC (rev 57336)
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+++ trunk/tests/phpunit/data/l10n/example-simple.mo       2024-01-23 13:32:34 UTC (rev 57337)
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/data/l10n/example-simple.mo
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span><a id="svnmimetype"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:mime-type</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+application/octet-stream
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunitdatal10nexamplesimplephp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/data/l10n/example-simple.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/data/l10n/example-simple.php                          (rev 0)
+++ trunk/tests/phpunit/data/l10n/example-simple.php    2024-01-23 13:32:34 UTC (rev 57337)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,10 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+return [
+       'messages' =>
+               [
+                       'original' => ['translation'],
+                       'contextoriginal with context' => ['translation with context'],
+                       'plural0' . "\0" . 'plural1' => ['translation0', 'translation1'],
+                       'contextplural0 with context' . "\0" . 'plural1 with context' => ['translation0 with context', 'translation1 with context'],
+               ],
+];
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/data/l10n/example-simple.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunitdatal10nexamplesimplepo"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/data/l10n/example-simple.po</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/data/l10n/example-simple.po                           (rev 0)
+++ trunk/tests/phpunit/data/l10n/example-simple.po     2024-01-23 13:32:34 UTC (rev 57337)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,23 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+msgid ""
+msgstr ""
+"PO-Revision-Date: 2016-01-05 18:45:32+1000\n"
+
+msgid "original"
+msgstr "translation"
+
+msgctxt "context"
+msgid "original with context"
+msgstr "translation with context"
+
+msgid "plural0"
+msgid_plural "plural1"
+msgstr[0] "translation0"
+msgstr[1] "translation1"
+
+msgctxt "context"
+msgid "plural0 with context"
+msgid_plural "plural1 with context"
+msgstr[0] "translation0 with context"
+msgstr[1] "translation1 with context"
+
+
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/data/l10n/example-simple.po
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunitdatal10nfa_IRmo"></a>
<div class="binary"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/data/l10n/fa_IR.mo</h4>
<pre class="diff"><span>
<span class="cx">(Binary files differ)
</span></span></pre></div>
<span class="cx" style="display: block; padding: 0 10px">Index: trunk/tests/phpunit/data/l10n/fa_IR.mo
</span><span class="cx" style="display: block; padding: 0 10px">===================================================================
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">--- trunk/tests/phpunit/data/l10n/fa_IR.mo       2024-01-23 13:05:08 UTC (rev 57336)
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+++ trunk/tests/phpunit/data/l10n/fa_IR.mo        2024-01-23 13:32:34 UTC (rev 57337)
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/data/l10n/fa_IR.mo
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span><a id="svnmimetype"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:mime-type</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+application/octet-stream
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunitdatal10npluralmo"></a>
<div class="binary"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/data/l10n/plural.mo</h4>
<pre class="diff"><span>
<span class="cx">(Binary files differ)
</span></span></pre></div>
<span class="cx" style="display: block; padding: 0 10px">Index: trunk/tests/phpunit/data/l10n/plural.mo
</span><span class="cx" style="display: block; padding: 0 10px">===================================================================
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">--- trunk/tests/phpunit/data/l10n/plural.mo      2024-01-23 13:05:08 UTC (rev 57336)
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+++ trunk/tests/phpunit/data/l10n/plural.mo       2024-01-23 13:32:34 UTC (rev 57337)
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/data/l10n/plural.mo
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span><a id="svnmimetype"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:mime-type</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+application/octet-stream
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunitdatal10nsimplemo"></a>
<div class="binary"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/data/l10n/simple.mo</h4>
<pre class="diff"><span>
<span class="cx">(Binary files differ)
</span></span></pre></div>
<span class="cx" style="display: block; padding: 0 10px">Index: trunk/tests/phpunit/data/l10n/simple.mo
</span><span class="cx" style="display: block; padding: 0 10px">===================================================================
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">--- trunk/tests/phpunit/data/l10n/simple.mo      2024-01-23 13:05:08 UTC (rev 57336)
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+++ trunk/tests/phpunit/data/l10n/simple.mo       2024-01-23 13:32:34 UTC (rev 57337)
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/data/l10n/simple.mo
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span><a id="svnmimetype"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:mime-type</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+application/octet-stream
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunitdatapomosimplel10nphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/data/pomo/simple.l10n.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/data/pomo/simple.l10n.php                             (rev 0)
+++ trunk/tests/phpunit/data/pomo/simple.l10n.php       2024-01-23 13:32:34 UTC (rev 57337)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,3 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+return ['project-id-version'=>'WordPress 2.6-bleeding','report-msgid-bugs-to'=>'wp-polyglots@lists.automattic.com','messages'=>['baba'=>'dyado','kuku
+ruku'=>'yes']];
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/data/pomo/simple.l10n.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsl10nloadTextdomainJustInTimephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/l10n/loadTextdomainJustInTime.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/l10n/loadTextdomainJustInTime.php       2024-01-23 13:05:08 UTC (rev 57336)
+++ trunk/tests/phpunit/tests/l10n/loadTextdomainJustInTime.php 2024-01-23 13:32:34 UTC (rev 57337)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -48,6 +48,9 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $wp_textdomain_registry = new WP_Textdomain_Registry();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                unload_textdomain( 'internationalized-plugin' );
+               unload_textdomain( 'internationalized-theme' );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 parent::tear_down();
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunktestsphpunittestsl10nwpLocaleSwitcherphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/l10n/wpLocaleSwitcher.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/l10n/wpLocaleSwitcher.php       2024-01-23 13:05:08 UTC (rev 57336)
+++ trunk/tests/phpunit/tests/l10n/wpLocaleSwitcher.php 2024-01-23 13:32:34 UTC (rev 57337)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -21,6 +21,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        protected static $user_id;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        /**
+        * @var WP_Locale_Switcher
+        */
+       protected $orig_instance;
+
</ins><span class="cx" style="display: block; padding: 0 10px">         public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
</span><span class="cx" style="display: block; padding: 0 10px">                self::$user_id = $factory->user->create(
</span><span class="cx" style="display: block; padding: 0 10px">                        array(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -42,7 +47,11 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $wp_textdomain_registry = new WP_Textdomain_Registry();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                remove_filter( 'locale', array( $wp_locale_switcher, 'filter_locale' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->orig_instance = $wp_locale_switcher;
+
+               remove_all_filters( 'locale' );
+               remove_all_filters( 'determine_locale' );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $wp_locale_switcher = new WP_Locale_Switcher();
</span><span class="cx" style="display: block; padding: 0 10px">                $wp_locale_switcher->init();
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -58,10 +67,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">                // before resetting $wp_locale_switcher.
</span><span class="cx" style="display: block; padding: 0 10px">                restore_current_locale();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                remove_filter( 'locale', array( $wp_locale_switcher, 'filter_locale' ) );
-               $wp_locale_switcher = new WP_Locale_Switcher();
-               $wp_locale_switcher->init();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         remove_all_filters( 'locale' );
+               remove_all_filters( 'determine_locale' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $wp_locale_switcher = $this->orig_instance;
+
+               unload_textdomain( 'internationalized-plugin' );
+               unload_textdomain( 'custom-internationalized-theme' );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 parent::tear_down();
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunktestsphpunittestsl10nwpTranslationControllerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/l10n/wpTranslationController.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/l10n/wpTranslationController.php                                (rev 0)
+++ trunk/tests/phpunit/tests/l10n/wpTranslationController.php  2024-01-23 13:32:34 UTC (rev 57337)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,356 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+/**
+ * @group l10n
+ * @group i18n
+ */
+class WP_Translation_Controller_Tests extends WP_UnitTestCase {
+       /**
+        * @return void
+        */
+       public function tear_down() {
+               remove_all_filters( 'translation_file_format' );
+               unload_textdomain( 'wp-tests-domain' );
+               unload_textdomain( 'internationalized-plugin' );
+
+               parent::tear_down();
+       }
+
+       /**
+        * @covers ::load_textdomain
+        * @covers WP_Translation_Controller::get_entries
+        * @covers WP_Translation_Controller::get_headers
+        * @covers WP_Translation_Controller::normalize_header
+        *
+        * @return void
+        */
+       public function test_load_textdomain() {
+               global $l10n;
+
+               $loaded_before_load = is_textdomain_loaded( 'wp-tests-domain' );
+
+               $load_successful = load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/simple.mo' );
+
+               $loaded_after_load = is_textdomain_loaded( 'wp-tests-domain' );
+
+               $compat_instance = $l10n['wp-tests-domain'] ?? null;
+
+               $is_loaded = WP_Translation_Controller::instance()->is_textdomain_loaded( 'wp-tests-domain' );
+               $headers   = WP_Translation_Controller::instance()->get_headers( 'wp-tests-domain' );
+               $entries   = WP_Translation_Controller::instance()->get_entries( 'wp-tests-domain' );
+
+               $unload_successful = unload_textdomain( 'wp-tests-domain' );
+
+               $loaded_after_unload = is_textdomain_loaded( 'wp-tests-domain' );
+
+               $this->assertFalse( $loaded_before_load, 'Text domain was already loaded at beginning of the test' );
+               $this->assertTrue( $load_successful, 'Text domain not successfully loaded' );
+               $this->assertTrue( $loaded_after_load, 'Text domain is not considered loaded' );
+               $this->assertInstanceOf( WP_Translations::class, $compat_instance, 'No compat provider instance used' );
+               $this->assertTrue( $unload_successful, 'Text domain not successfully unloaded' );
+               $this->assertFalse( $loaded_after_unload, 'Text domain still considered loaded after unload' );
+               $this->assertTrue( $is_loaded, 'Text domain not considered loaded' );
+               $this->assertEqualSetsWithIndex(
+                       array(
+                               'Project-Id-Version'   => 'WordPress 2.6-bleeding',
+                               'Report-Msgid-Bugs-To' => 'wp-polyglots@lists.automattic.com',
+                       ),
+                       $headers,
+                       'Actual translation headers do not match expected ones'
+               );
+               $this->assertEqualSetsWithIndex(
+                       array(
+                               'baba'       => 'dyado',
+                               "kuku\nruku" => 'yes',
+                       ),
+                       $entries,
+                       'Actual translation entries do not match expected ones'
+               );
+       }
+
+       /**
+        * @covers ::load_textdomain
+        * @covers WP_Translation_Controller::get_entries
+        * @covers WP_Translation_Controller::get_headers
+        * @covers WP_Translation_Controller::normalize_header
+        *
+        * @return void
+        */
+       public function test_load_textdomain_existing_override() {
+               add_filter( 'override_load_textdomain', '__return_true' );
+
+               load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/simple.mo' );
+
+               $is_loaded_wp = is_textdomain_loaded( 'wp-tests-domain' );
+
+               $is_loaded = WP_Translation_Controller::instance()->is_textdomain_loaded( 'wp-tests-domain' );
+
+               remove_filter( 'override_load_textdomain', '__return_true' );
+
+               $this->assertFalse( $is_loaded_wp );
+               $this->assertFalse( $is_loaded );
+       }
+
+       /**
+        * @covers ::load_textdomain
+        *
+        * @return void
+        */
+       public function test_load_textdomain_php_files() {
+               $load_php_successful = load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/simple.l10n.php' );
+
+               $unload_php_successful = unload_textdomain( 'wp-tests-domain' );
+
+               $this->assertTrue( $load_php_successful, 'PHP file not successfully loaded' );
+               $this->assertTrue( $unload_php_successful );
+       }
+
+       /**
+        * @covers ::load_textdomain
+        *
+        * @return void
+        */
+       public function test_load_textdomain_reads_php_files_if_filtered_format_is_unsupported() {
+               add_filter(
+                       'translation_file_format',
+                       static function () {
+                               return 'unknown-format';
+                       }
+               );
+
+               $load_mo_successful = load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/simple.mo' );
+
+               $unload_mo_successful = unload_textdomain( 'wp-tests-domain' );
+
+               $load_php_successful = load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/simple.l10n.php' );
+
+               $unload_php_successful = unload_textdomain( 'wp-tests-domain' );
+
+               $this->assertTrue( $load_mo_successful, 'MO file not successfully loaded' );
+               $this->assertTrue( $unload_mo_successful );
+               $this->assertTrue( $load_php_successful, 'PHP file not successfully loaded' );
+               $this->assertTrue( $unload_php_successful );
+       }
+
+       /**
+        * @covers ::load_textdomain
+        *
+        * @return void
+        */
+       public function test_load_textdomain_existing_translation_is_kept() {
+               global $l10n;
+
+               load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/simple.mo' );
+
+               load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/context.mo' );
+
+               $mo = new MO();
+               $mo->import_from_file( DIR_TESTDATA . '/pomo/context.mo' );
+               $mo->merge_with( $l10n['wp-tests-domain'] );
+               $l10n['wp-tests-domain'] = $mo;
+
+               $simple  = __( 'baba', 'wp-tests-domain' );
+               $context = _x( 'one dragon', 'not so dragon', 'wp-tests-domain' );
+
+               $this->assertSame( 'dyado', $simple );
+               $this->assertSame( 'oney dragoney', $context );
+               $this->assertInstanceOf( Translations::class, $l10n['wp-tests-domain'] );
+       }
+
+       /**
+        * @covers ::load_textdomain
+        *
+        * @return void
+        */
+       public function test_load_textdomain_loads_existing_translation() {
+               global $l10n;
+
+               $mo = new MO();
+               $mo->import_from_file( DIR_TESTDATA . '/pomo/simple.mo' );
+               $l10n['wp-tests-domain'] = $mo;
+
+               load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/context.mo' );
+
+               $simple  = __( 'baba', 'wp-tests-domain' );
+               $context = _x( 'one dragon', 'not so dragon', 'wp-tests-domain' );
+
+               $this->assertSame( 'dyado', $simple );
+               $this->assertSame( 'oney dragoney', $context );
+               $this->assertInstanceOf( WP_Translations::class, $l10n['wp-tests-domain'] );
+       }
+
+       /**
+        * @covers ::load_textdomain
+        *
+        * @return void
+        */
+       public function test_load_textdomain_loads_existing_translation_mo_files() {
+               global $l10n;
+
+               add_filter(
+                       'translation_file_format',
+                       static function () {
+                               return 'mo';
+                       }
+               );
+
+               load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/simple.mo' );
+
+               $mo = new MO();
+               $mo->import_from_file( DIR_TESTDATA . '/pomo/simple.mo' );
+               $l10n['wp-tests-domain'] = $mo;
+
+               load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/context.mo' );
+
+               $simple  = __( 'baba', 'wp-tests-domain' );
+               $context = _x( 'one dragon', 'not so dragon', 'wp-tests-domain' );
+
+               $this->assertSame( 'dyado', $simple );
+               $this->assertSame( 'oney dragoney', $context );
+               $this->assertInstanceOf( WP_Translations::class, $l10n['wp-tests-domain'] );
+       }
+
+       /**
+        * @covers ::load_textdomain
+        *
+        * @return void
+        */
+       public function test_load_textdomain_loads_existing_translation_php_files() {
+               global $l10n;
+
+               // Just to ensure the PHP files exist.
+               load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/simple.mo' );
+               load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/context.mo' );
+               unload_textdomain( 'wp-tests-domain' );
+
+               load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/simple.mo' );
+
+               $mo = new MO();
+               $mo->import_from_file( DIR_TESTDATA . '/pomo/simple.mo' );
+               $l10n['wp-tests-domain'] = $mo;
+
+               load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/context.mo' );
+
+               $simple  = __( 'baba', 'wp-tests-domain' );
+               $context = _x( 'one dragon', 'not so dragon', 'wp-tests-domain' );
+
+               $this->assertSame( 'dyado', $simple );
+               $this->assertSame( 'oney dragoney', $context );
+               $this->assertInstanceOf( WP_Translations::class, $l10n['wp-tests-domain'] );
+       }
+
+       /**
+        * @covers ::unload_textdomain
+        * @covers WP_Translation_Controller::get_entries
+        * @covers WP_Translation_Controller::get_headers
+        * @covers WP_Translation_Controller::normalize_header
+        *
+        * @return void
+        */
+       public function test_unload_textdomain() {
+               global $l10n;
+
+               load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/simple.mo' );
+
+               $unload_successful = unload_textdomain( 'wp-tests-domain' );
+
+               $loaded_after_unload = is_textdomain_loaded( 'wp-tests-domain' );
+
+               $compat_instance = $l10n['wp-tests-domain'] ?? null;
+
+               $is_loaded = WP_Translation_Controller::instance()->is_textdomain_loaded( 'wp-tests-domain' );
+               $headers   = WP_Translation_Controller::instance()->get_headers( 'wp-tests-domain' );
+               $entries   = WP_Translation_Controller::instance()->get_entries( 'wp-tests-domain' );
+
+               $this->assertNull( $compat_instance, 'Compat instance was not removed' );
+               $this->assertTrue( $unload_successful, 'Text domain not successfully unloaded' );
+               $this->assertFalse( $loaded_after_unload, 'Text domain still considered loaded after unload' );
+               $this->assertFalse( $is_loaded, 'Text domain still considered loaded' );
+               $this->assertEmpty( $headers, 'Actual translation headers are not empty' );
+               $this->assertEmpty( $entries, 'Actual translation entries are not empty' );
+       }
+
+       /**
+        * @covers ::unload_textdomain
+        *
+        * @return void
+        */
+       public function test_unload_textdomain_existing_override() {
+               add_filter( 'override_unload_textdomain', '__return_true' );
+
+               load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/simple.mo' );
+
+               $unload_successful = unload_textdomain( 'wp-tests-domain' );
+
+               $is_loaded = WP_Translation_Controller::instance()->is_textdomain_loaded( 'wp-tests-domain' );
+
+               remove_filter( 'override_unload_textdomain', '__return_true' );
+
+               $unload_successful_after = unload_textdomain( 'wp-tests-domain' );
+
+               $is_loaded_after = WP_Translation_Controller::instance()->is_textdomain_loaded( 'wp-tests-domain' );
+
+               $this->assertTrue( $unload_successful );
+               $this->assertTrue( $is_loaded );
+               $this->assertTrue( $unload_successful_after );
+               $this->assertFalse( $is_loaded_after );
+       }
+
+       /**
+        * @covers ::load_textdomain
+        * @covers ::unload_textdomain
+        *
+        * @return void
+        */
+       public function test_switch_to_locale_translations_stay_loaded_default_textdomain() {
+               switch_to_locale( 'es_ES' );
+
+               $actual = __( 'Invalid parameter.' );
+
+               $this->assertTrue( WP_Translation_Controller::instance()->is_textdomain_loaded() );
+               $this->assertTrue( WP_Translation_Controller::instance()->is_textdomain_loaded( 'default', 'es_ES' ) );
+
+               restore_previous_locale();
+
+               $actual_2 = __( 'Invalid parameter.' );
+
+               $this->assertTrue( WP_Translation_Controller::instance()->is_textdomain_loaded( 'default', 'es_ES' ) );
+
+               $this->assertSame( 'Parámetro no válido. ', $actual );
+               $this->assertSame( 'Invalid parameter.', $actual_2 );
+       }
+
+       /**
+        * @covers ::load_textdomain
+        * @covers ::unload_textdomain
+        * @covers ::change_locale
+        *
+        * @return void
+        */
+       public function test_switch_to_locale_translations_stay_loaded_custom_textdomain() {
+               $this->assertSame( 'en_US', WP_Translation_Controller::instance()->get_locale() );
+
+               require_once DIR_TESTDATA . '/plugins/internationalized-plugin.php';
+
+               $before = i18n_plugin_test();
+
+               switch_to_locale( 'es_ES' );
+
+               $actual = i18n_plugin_test();
+
+               $this->assertSame( 'es_ES', WP_Translation_Controller::instance()->get_locale() );
+               $this->assertTrue( WP_Translation_Controller::instance()->is_textdomain_loaded( 'internationalized-plugin', 'es_ES' ) );
+               $this->assertTrue( WP_Translation_Controller::instance()->is_textdomain_loaded( 'default', 'es_ES' ) );
+               $this->assertFalse( WP_Translation_Controller::instance()->is_textdomain_loaded( 'foo-bar', 'es_ES' ) );
+
+               restore_previous_locale();
+
+               $after = i18n_plugin_test();
+
+               $this->assertTrue( WP_Translation_Controller::instance()->is_textdomain_loaded( 'internationalized-plugin', 'es_ES' ) );
+
+               $this->assertSame( 'This is a dummy plugin', $before );
+               $this->assertSame( 'Este es un plugin dummy', $actual );
+               $this->assertSame( 'This is a dummy plugin', $after );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/l10n/wpTranslationController.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsl10nwpTranslationsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/l10n/wpTranslations.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/l10n/wpTranslations.php                         (rev 0)
+++ trunk/tests/phpunit/tests/l10n/wpTranslations.php   2024-01-23 13:32:34 UTC (rev 57337)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,292 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+/**
+ * @coversDefaultClass WP_Translations
+ * @group l10n
+ * @group i18n
+ */
+class WP_Translations_Tests extends WP_UnitTestCase {
+       /**
+        * @return void
+        */
+       public function tear_down() {
+               unload_textdomain( 'wp-tests-domain' );
+
+               parent::tear_down();
+       }
+
+       /**
+        * @covers ::__construct
+        * @covers ::__get
+        * @covers ::make_entry
+        *
+        * @return void
+        */
+       public function test_get_entries() {
+               global $l10n;
+
+               load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/simple.mo' );
+
+               $compat_instance = $l10n['wp-tests-domain'] ?? null;
+
+               $entries = $compat_instance ? $compat_instance->entries : array();
+
+               $unload_successful = unload_textdomain( 'wp-tests-domain' );
+
+               $this->assertInstanceOf( WP_Translations::class, $compat_instance, 'No compat provider instance used' );
+               $this->assertTrue( $unload_successful, 'Text domain not successfully unloaded' );
+               $this->assertEqualSets(
+                       array(
+                               new Translation_Entry(
+                                       array(
+                                               'singular'     => 'baba',
+                                               'translations' => array( 'dyado' ),
+                                       )
+                               ),
+                               new Translation_Entry(
+                                       array(
+                                               'singular'     => "kuku\nruku",
+                                               'translations' => array( 'yes' ),
+                                       )
+                               ),
+                       ),
+                       $entries,
+                       'Actual translation entries do not match expected ones'
+               );
+       }
+
+       /**
+        * @covers ::__get
+        * @covers ::make_entry
+        *
+        * @return void
+        */
+       public function test_get_entries_plural() {
+               global $l10n;
+
+               load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/plural.mo' );
+
+               $compat_instance = $l10n['wp-tests-domain'] ?? null;
+
+               $entries = $compat_instance ? $compat_instance->entries : array();
+
+               $unload_successful = unload_textdomain( 'wp-tests-domain' );
+
+               $this->assertInstanceOf( WP_Translations::class, $compat_instance, 'No compat provider instance used' );
+               $this->assertTrue( $unload_successful, 'Text domain not successfully unloaded' );
+               $this->assertEqualSets(
+                       array(
+                               new Translation_Entry(
+                                       array(
+                                               'singular'     => 'one dragon',
+                                               'plural'       => '%d dragons',
+                                               'translations' => array(
+                                                       'oney dragoney',
+                                                       'twoey dragoney',
+                                                       'manyey dragoney',
+                                                       'manyeyey dragoney',
+                                                       'manyeyeyey dragoney',
+                                               ),
+                                       )
+                               ),
+                       ),
+                       $entries,
+                       'Actual translation entries do not match expected ones'
+               );
+       }
+
+
+       /**
+        * @covers ::__get
+        * @covers ::make_entry
+        *
+        * @return void
+        */
+       public function test_get_entries_context() {
+               global $l10n;
+
+               load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/context.mo' );
+
+               $compat_instance = $l10n['wp-tests-domain'] ?? null;
+
+               $entries = $compat_instance ? $compat_instance->entries : array();
+
+               $unload_successful = unload_textdomain( 'wp-tests-domain' );
+
+               $this->assertInstanceOf( WP_Translations::class, $compat_instance, 'No compat provider instance used' );
+               $this->assertTrue( $unload_successful, 'Text domain not successfully unloaded' );
+               $this->assertEqualSets(
+                       array(
+                               new Translation_Entry(
+                                       array(
+                                               'context'      => 'not so dragon',
+                                               'singular'     => 'one dragon',
+                                               'translations' => array( 'oney dragoney' ),
+                                       )
+                               ),
+                               new Translation_Entry(
+                                       array(
+                                               'is_plural'    => true,
+                                               'singular'     => 'one dragon',
+                                               'plural'       => '%d dragons',
+                                               'context'      => 'dragonland',
+                                               'translations' => array(
+                                                       'oney dragoney',
+                                                       'twoey dragoney',
+                                                       'manyey dragoney',
+                                               ),
+                                       )
+                               ),
+                       ),
+                       $entries,
+                       'Actual translation entries do not match expected ones'
+               );
+       }
+
+       /**
+        * @covers ::__get
+        *
+        * @return void
+        */
+       public function test_get_headers() {
+               global $l10n;
+
+               $load_successful = load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/simple.mo' );
+
+               $compat_instance = $l10n['wp-tests-domain'] ?? null;
+
+               $headers = $compat_instance ? $compat_instance->headers : array();
+
+               $unload_successful = unload_textdomain( 'wp-tests-domain' );
+
+               $this->assertTrue( $load_successful, 'Text domain not successfully loaded' );
+               $this->assertInstanceOf( WP_Translations::class, $compat_instance, 'No compat provider instance used' );
+               $this->assertTrue( $unload_successful, 'Text domain not successfully unloaded' );
+               $this->assertEqualSetsWithIndex(
+                       array(
+                               'Project-Id-Version'   => 'WordPress 2.6-bleeding',
+                               'Report-Msgid-Bugs-To' => 'wp-polyglots@lists.automattic.com',
+                       ),
+                       $headers,
+                       'Actual translation headers do not match expected ones'
+               );
+       }
+
+       /**
+        * @covers ::__get
+        *
+        * @return void
+        */
+       public function test_getter_unsupported_property() {
+               global $l10n;
+
+               load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/simple.mo' );
+
+               $compat_instance = $l10n['wp-tests-domain'] ?? null;
+
+               $this->assertInstanceOf( WP_Translations::class, $compat_instance );
+
+               $this->assertNull( $compat_instance->foo );
+       }
+
+       /**
+        * @covers ::translate
+        *
+        * @return void
+        */
+       public function test_translate() {
+               global $l10n;
+
+               load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/simple.mo' );
+
+               $compat_instance = $l10n['wp-tests-domain'] ?? null;
+
+               $translation         = $compat_instance ? $compat_instance->translate( 'baba' ) : false;
+               $translation_missing = $compat_instance ? $compat_instance->translate( 'does not exist' ) : false;
+
+               $unload_successful = unload_textdomain( 'wp-tests-domain' );
+
+               $this->assertInstanceOf( WP_Translations::class, $compat_instance, 'No compat provider instance used' );
+               $this->assertSame( 'dyado', $translation, 'Actual translation does not match expected one' );
+               $this->assertSame( 'does not exist', $translation_missing, 'Actual translation fallback does not match expected one' );
+               $this->assertTrue( $unload_successful, 'Text domain not successfully unloaded' );
+       }
+
+       /**
+        * @covers ::translate_plural
+        *
+        * @return void
+        */
+       public function test_translate_plural() {
+               global $l10n;
+
+               load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/plural.mo' );
+
+               $compat_instance = $l10n['wp-tests-domain'] ?? null;
+
+               $translation_1       = $compat_instance ? $compat_instance->translate_plural( 'one dragon', '%d dragons', 1 ) : false;
+               $translation_2       = $compat_instance ? $compat_instance->translate_plural( 'one dragon', '%d dragons', 2 ) : false;
+               $translation_minus_8 = $compat_instance ? $compat_instance->translate_plural( 'one dragon', '%d dragons', -8 ) : false;
+
+               $unload_successful = unload_textdomain( 'wp-tests-domain' );
+
+               $this->assertInstanceOf( WP_Translations::class, $compat_instance, 'No compat provider instance used' );
+               $this->assertSame( 'oney dragoney', $translation_1, 'Actual translation does not match expected one' );
+               $this->assertSame( 'twoey dragoney', $translation_2, 'Actual translation does not match expected one' );
+               $this->assertSame( 'twoey dragoney', $translation_minus_8, 'Actual translation does not match expected one' );
+               $this->assertTrue( $unload_successful, 'Text domain not successfully unloaded' );
+       }
+
+       /**
+        * @covers ::translate_plural
+        *
+        * @return void
+        */
+       public function test_translate_plural_missing() {
+               global $l10n;
+
+               load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/plural.mo' );
+
+               $compat_instance = $l10n['wp-tests-domain'] ?? null;
+
+               $translation_1 = $compat_instance ? $compat_instance->translate_plural( '%d house', '%d houses', 1 ) : false;
+               $translation_2 = $compat_instance ? $compat_instance->translate_plural( '%d car', '%d cars', 2 ) : false;
+
+               $unload_successful = unload_textdomain( 'wp-tests-domain' );
+
+               $this->assertInstanceOf( WP_Translations::class, $compat_instance, 'No compat provider instance used' );
+               $this->assertSame( '%d house', $translation_1, 'Actual translation fallback does not match expected one' );
+               $this->assertSame( '%d cars', $translation_2, 'Actual plural translation fallback does not match expected one' );
+               $this->assertTrue( $unload_successful, 'Text domain not successfully unloaded' );
+       }
+
+       /**
+        * @covers ::translate
+        * @covers ::translate_plural
+        *
+        * @ticket 41257
+        *
+        * @return void
+        */
+       public function test_translate_invalid_edge_cases() {
+               load_textdomain( 'wp-tests-domain', DIR_TESTDATA . '/pomo/simple.mo' );
+
+               // phpcs:disable WordPress.WP.I18n
+               $null_string   = __( null, 'wp-tests-domain' );
+               $null_singular = _n( null, 'plural', 1, 'wp-tests-domain' );
+               $null_plural   = _n( 'singular', null, 1, 'wp-tests-domain' );
+               $null_both     = _n( null, null, 1, 'wp-tests-domain' );
+               $null_context  = _x( 'foo', null, 'wp-tests-domain' );
+               $float_number  = _n( '%d house', '%d houses', 7.5, 'wp-tests-domain' );
+               // phpcs:enable WordPress.WP.I18n
+
+               unload_textdomain( 'wp-tests-domain' );
+
+               $this->assertNull( $null_string );
+               $this->assertNull( $null_singular );
+               $this->assertSame( 'singular', $null_plural );
+               $this->assertNull( $null_both );
+               $this->assertSame( 'foo', $null_context );
+               $this->assertSame( '%d houses', $float_number );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/l10n/wpTranslations.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsl10nwpTranslationsConvertphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/l10n/wpTranslationsConvert.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/l10n/wpTranslationsConvert.php                          (rev 0)
+++ trunk/tests/phpunit/tests/l10n/wpTranslationsConvert.php    2024-01-23 13:32:34 UTC (rev 57337)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,598 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+/**
+ * @coversDefaultClass WP_Translation_Controller
+ * @group l10n
+ * @group i18n
+ */
+class WP_Translation_Controller_Convert_Tests extends WP_UnitTestCase {
+       /**
+        * @covers ::instance
+        *
+        * @return void
+        */
+       public function test_get_instance() {
+               $instance  = WP_Translation_Controller::instance();
+               $instance2 = WP_Translation_Controller::instance();
+
+               $this->assertSame( $instance, $instance2 );
+       }
+
+       /**
+        * @return void
+        */
+       public function test_no_files_loaded_returns_false() {
+               $instance = new WP_Translation_Controller();
+               $this->assertFalse( $instance->translate( 'singular' ) );
+               $this->assertFalse( $instance->translate_plural( array( 'plural0', 'plural1' ), 1 ) );
+       }
+
+       /**
+        * @covers ::unload
+        *
+        * @return void
+        */
+       public function test_unload_not_loaded() {
+               $instance = new WP_Translation_Controller();
+               $this->assertFalse( $instance->is_textdomain_loaded( 'unittest' ) );
+               $this->assertFalse( $instance->unload_textdomain( 'unittest' ) );
+       }
+
+       /**
+        * @covers ::load
+        * @covers ::unload
+        * @covers ::is_textdomain_loaded
+        * @covers ::translate
+        * @covers ::locate_translation
+        * @covers ::get_files
+        *
+        * @return void
+        */
+       public function test_unload_entire_textdomain() {
+               $instance = new WP_Translation_Controller();
+               $this->assertFalse( $instance->is_textdomain_loaded( 'unittest' ) );
+               $this->assertTrue( $instance->load_file( DIR_TESTDATA . '/l10n/example-simple.php', 'unittest' ) );
+               $this->assertTrue( $instance->is_textdomain_loaded( 'unittest' ) );
+
+               $this->assertSame( 'translation', $instance->translate( 'original', '', 'unittest' ) );
+
+               $this->assertTrue( $instance->unload_textdomain( 'unittest' ) );
+               $this->assertFalse( $instance->is_textdomain_loaded( 'unittest' ) );
+               $this->assertFalse( $instance->translate( 'original', '', 'unittest' ) );
+       }
+
+       /**
+        * @covers ::unload
+        * @covers WP_Translation_File::get_file
+        *
+        * @return void
+        */
+       public function test_unload_file_is_not_actually_loaded() {
+               $controller = new WP_Translation_Controller();
+               $this->assertTrue( $controller->load_file( DIR_TESTDATA . '/l10n/example-simple.mo', 'unittest' ) );
+               $this->assertFalse( $controller->unload_file( DIR_TESTDATA . '/l10n/simple.mo', 'unittest' ) );
+
+               $this->assertTrue( $controller->is_textdomain_loaded( 'unittest' ) );
+               $this->assertSame( 'translation', $controller->translate( 'original', '', 'unittest' ) );
+       }
+
+       /**
+        * @covers ::unload
+        * @covers ::is_textdomain_loaded
+        *
+        * @return void
+        */
+       public function test_unload_specific_locale() {
+               $instance = new WP_Translation_Controller();
+               $this->assertFalse( $instance->is_textdomain_loaded( 'unittest' ) );
+               $this->assertTrue( $instance->load_file( DIR_TESTDATA . '/l10n/example-simple.php', 'unittest' ) );
+               $this->assertTrue( $instance->is_textdomain_loaded( 'unittest' ) );
+
+               $this->assertFalse( $instance->is_textdomain_loaded( 'unittest', 'es_ES' ) );
+               $this->assertTrue( $instance->load_file( DIR_TESTDATA . '/l10n/example-simple.php', 'unittest', 'es_ES' ) );
+               $this->assertTrue( $instance->is_textdomain_loaded( 'unittest', 'es_ES' ) );
+
+               $this->assertSame( 'translation', $instance->translate( 'original', '', 'unittest' ) );
+               $this->assertSame( 'translation', $instance->translate( 'original', '', 'unittest', 'es_ES' ) );
+
+               $this->assertTrue( $instance->unload_textdomain( 'unittest', $instance->get_locale() ) );
+               $this->assertFalse( $instance->is_textdomain_loaded( 'unittest' ) );
+               $this->assertFalse( $instance->translate( 'original', '', 'unittest' ) );
+
+               $this->assertTrue( $instance->is_textdomain_loaded( 'unittest', 'es_ES' ) );
+               $this->assertTrue( $instance->unload_textdomain( 'unittest', 'es_ES' ) );
+               $this->assertFalse( $instance->is_textdomain_loaded( 'unittest', 'es_ES' ) );
+               $this->assertFalse( $instance->translate( 'original', '', 'unittest', 'es_ES' ) );
+       }
+
+       /**
+        * @dataProvider data_invalid_files
+        *
+        * @param string $type
+        * @param string $file_contents
+        * @param string|bool $expected_error
+        * @return void
+        *
+        * @phpstan-param 'mo'|'php' $type
+        */
+       public function test_invalid_files( string $type, string $file_contents, $expected_error = null ) {
+               $file = $this->temp_filename();
+
+               $this->assertNotFalse( $file );
+
+               file_put_contents( $file, $file_contents );
+
+               $instance = WP_Translation_File::create( $file, $type );
+
+               $this->assertInstanceOf( WP_Translation_File::class, $instance );
+
+               // Not an error condition until it attempts to parse the file.
+               $this->assertNull( $instance->error() );
+
+               // Trigger parsing.
+               $instance->headers();
+
+               $this->assertNotNull( $instance->error() );
+
+               if ( null !== $expected_error ) {
+                       $this->assertSame( $expected_error, $instance->error() );
+               }
+       }
+
+       /**
+        * @return array{0: array{0: 'mo'|'php', 1: string|false, 2?: string}}
+        */
+       public function data_invalid_files(): array {
+               return array(
+                       array( 'php', '' ),
+                       array( 'php', '<?php // This is a php file without a payload' ),
+                       array( 'mo', '', 'Invalid data' ),
+                       array( 'mo', 'Random data in a file long enough to be a real header', 'Magic marker does not exist' ),
+                       array( 'mo', pack( 'V*', 0x950412de ), 'Invalid data' ),
+                       array( 'mo', pack( 'V*', 0x950412de ) . 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'Unsupported revision' ),
+                       array( 'mo', pack( 'V*', 0x950412de, 0x0 ) . 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'Invalid data' ),
+               );
+       }
+
+       /**
+        * @covers WP_Translation_Controller::load
+        * @covers WP_Translation_Controller::is_textdomain_loaded
+        *
+        * @return void
+        */
+       public function test_load_non_existent_file() {
+               $instance = new WP_Translation_Controller();
+
+               $this->assertFalse( $instance->load_file( DIR_TESTDATA . '/l10n/file-that-doesnt-exist.mo', 'unittest' ) );
+               $this->assertFalse( $instance->is_textdomain_loaded( 'unittest' ) );
+       }
+
+       /**
+        * @covers WP_Translation_File::create
+        *
+        * @return void
+        */
+       public function test_create_non_existent_file() {
+               $this->assertFalse( WP_Translation_File::create( 'this-file-does-not-exist' ) );
+       }
+
+       /**
+        * @covers WP_Translation_File::create
+        *
+        * @return void
+        */
+       public function test_create_invalid_filetype() {
+               $file = $this->temp_filename();
+               $this->assertNotFalse( $file );
+               file_put_contents( $file, '' );
+               $this->assertFalse( WP_Translation_File::create( $file, 'invalid' ) );
+       }
+
+       /**
+        * @covers ::load
+        * @covers ::is_textdomain_loaded
+        * @covers ::translate
+        * @covers ::translate_plural
+        * @covers ::locate_translation
+        * @covers ::get_files
+        *
+        * @dataProvider data_simple_example_files
+        *
+        * @param string $file
+        * @return void
+        */
+       public function test_simple_translation_files( string $file ) {
+               $controller = new WP_Translation_Controller();
+               $this->assertTrue( $controller->load_file( DIR_TESTDATA . '/l10n/' . $file, 'unittest' ) );
+
+               $this->assertTrue( $controller->is_textdomain_loaded( 'unittest' ) );
+               $this->assertFalse( $controller->is_textdomain_loaded( 'textdomain not loaded' ) );
+
+               $this->assertFalse( $controller->translate( "string that doesn't exist", '', 'unittest' ) );
+               $this->assertFalse( $controller->translate( 'original', '', 'textdomain not loaded' ) );
+
+               $this->assertSame( 'translation', $controller->translate( 'original', '', 'unittest' ) );
+               $this->assertSame( 'translation with context', $controller->translate( 'original with context', 'context', 'unittest' ) );
+
+               $this->assertSame( 'translation1', $controller->translate_plural( array( 'plural0', 'plural1' ), 0, '', 'unittest' ) );
+               $this->assertSame( 'translation0', $controller->translate_plural( array( 'plural0', 'plural1' ), 1, '', 'unittest' ) );
+               $this->assertSame( 'translation1', $controller->translate_plural( array( 'plural0', 'plural1' ), 2, '', 'unittest' ) );
+
+               $this->assertSame( 'translation1 with context', $controller->translate_plural( array( 'plural0 with context', 'plural1 with context' ), 0, 'context', 'unittest' ) );
+               $this->assertSame( 'translation0 with context', $controller->translate_plural( array( 'plural0 with context', 'plural1 with context' ), 1, 'context', 'unittest' ) );
+               $this->assertSame( 'translation1 with context', $controller->translate_plural( array( 'plural0 with context', 'plural1 with context' ), 2, 'context', 'unittest' ) );
+       }
+
+       /**
+        * @return array<array{0: string}>
+        */
+       public function data_simple_example_files(): array {
+               return array(
+                       array( 'example-simple.mo' ),
+                       array( 'example-simple.php' ),
+               );
+       }
+
+       /**
+        * @covers ::load
+        * @covers ::unload
+        * @covers ::is_textdomain_loaded
+        * @covers ::translate
+        * @covers ::translate_plural
+        * @covers ::locate_translation
+        * @covers ::get_files
+        * @covers WP_Translation_File::get_plural_form
+        * @covers WP_Translation_File::make_plural_form_function
+        *
+        * @return void
+        */
+       public function test_load_multiple_files() {
+               $controller = new WP_Translation_Controller();
+               $this->assertTrue( $controller->load_file( DIR_TESTDATA . '/l10n/example-simple.mo', 'unittest' ) );
+               $this->assertTrue( $controller->load_file( DIR_TESTDATA . '/l10n/simple.mo', 'unittest' ) );
+               $this->assertTrue( $controller->load_file( DIR_TESTDATA . '/l10n/plural.mo', 'unittest' ) );
+
+               $this->assertTrue( $controller->is_textdomain_loaded( 'unittest' ) );
+
+               $this->assertFalse( $controller->translate( "string that doesn't exist", '', 'unittest' ) );
+               $this->assertFalse( $controller->translate( 'original', '', 'textdomain not loaded' ) );
+
+               // From example-simple.mo
+
+               $this->assertSame( 'translation', $controller->translate( 'original', '', 'unittest' ) );
+               $this->assertSame( 'translation with context', $controller->translate( 'original with context', 'context', 'unittest' ) );
+
+               $this->assertSame( 'translation1', $controller->translate_plural( array( 'plural0', 'plural1' ), 0, '', 'unittest' ) );
+               $this->assertSame( 'translation0', $controller->translate_plural( array( 'plural0', 'plural1' ), 1, '', 'unittest' ) );
+               $this->assertSame( 'translation1', $controller->translate_plural( array( 'plural0', 'plural1' ), 2, '', 'unittest' ) );
+
+               $this->assertSame( 'translation1 with context', $controller->translate_plural( array( 'plural0 with context', 'plural1 with context' ), 0, 'context', 'unittest' ) );
+               $this->assertSame( 'translation0 with context', $controller->translate_plural( array( 'plural0 with context', 'plural1 with context' ), 1, 'context', 'unittest' ) );
+               $this->assertSame( 'translation1 with context', $controller->translate_plural( array( 'plural0 with context', 'plural1 with context' ), 2, 'context', 'unittest' ) );
+
+               // From simple.mo.
+
+               $this->assertSame( 'dyado', $controller->translate( 'baba', '', 'unittest' ) );
+
+               // From plural.mo.
+
+               $this->assertSame( 'oney dragoney', $controller->translate_plural( array( 'one dragon', '%d dragons' ), 1, '', 'unittest' ), 'Actual translation does not match expected one' );
+               $this->assertSame( 'twoey dragoney', $controller->translate_plural( array( 'one dragon', '%d dragons' ), 2, '', 'unittest' ), 'Actual translation does not match expected one' );
+               $this->assertSame( 'twoey dragoney', $controller->translate_plural( array( 'one dragon', '%d dragons' ), -8, '', 'unittest' ), 'Actual translation does not match expected one' );
+
+               $this->assertTrue( $controller->unload_file( DIR_TESTDATA . '/l10n/simple.mo', 'unittest' ) );
+
+               $this->assertFalse( $controller->translate( 'baba', '', 'unittest' ) );
+       }
+
+       /**
+        * @covers ::set_locale
+        * @covers ::get_locale
+        * @covers ::load
+        * @covers ::unload
+        * @covers ::is_textdomain_loaded
+        * @covers ::translate
+        * @covers ::translate_plural
+        *
+        * @return void
+        */
+       public function test_load_multiple_locales() {
+               $controller = new WP_Translation_Controller();
+
+               $this->assertSame( 'en_US', $controller->get_locale() );
+
+               $controller->set_locale( 'de_DE' );
+
+               $this->assertSame( 'de_DE', $controller->get_locale() );
+
+               $this->assertTrue( $controller->load_file( DIR_TESTDATA . '/l10n/example-simple.mo', 'unittest' ) );
+               $this->assertTrue( $controller->load_file( DIR_TESTDATA . '/l10n/simple.mo', 'unittest', 'es_ES' ) );
+               $this->assertTrue( $controller->load_file( DIR_TESTDATA . '/l10n/plural.mo', 'unittest', 'en_US' ) );
+
+               $this->assertTrue( $controller->is_textdomain_loaded( 'unittest' ) );
+
+               // From example-simple.mo
+
+               $this->assertSame( 'translation', $controller->translate( 'original', '', 'unittest' ), 'String should be translated in de_DE' );
+               $this->assertFalse( $controller->translate( 'original', '', 'unittest', 'es_ES' ), 'String should not be translated in es_ES' );
+               $this->assertFalse( $controller->translate( 'original', '', 'unittest', 'en_US' ), 'String should not be translated in en_US' );
+
+               // From simple.mo.
+
+               $this->assertFalse( $controller->translate( 'baba', '', 'unittest' ), 'String should not be translated in de_DE' );
+               $this->assertSame( 'dyado', $controller->translate( 'baba', '', 'unittest', 'es_ES' ), 'String should be translated in es_ES' );
+               $this->assertFalse( $controller->translate( 'baba', '', 'unittest', 'en_US' ), 'String should not be translated in en_US' );
+
+               $this->assertTrue( $controller->unload_file( DIR_TESTDATA . '/l10n/plural.mo', 'unittest', 'de_DE' ) );
+
+               $this->assertSame( 'oney dragoney', $controller->translate_plural( array( 'one dragon', '%d dragons' ), 1, '', 'unittest', 'en_US' ), 'String should be translated in en_US' );
+
+               $this->assertTrue( $controller->unload_file( DIR_TESTDATA . '/l10n/plural.mo', 'unittest', 'en_US' ) );
+
+               $this->assertFalse( $controller->translate_plural( array( 'one dragon', '%d dragons' ), 1, '', 'unittest', 'en_US' ), 'String should not be translated in en_US' );
+       }
+
+       /**
+        * @covers ::unload
+        *
+        * @return void
+        */
+       public function test_unload_with_multiple_locales() {
+               $ginger_mo = new WP_Translation_Controller();
+
+               $ginger_mo->set_locale( 'de_DE' );
+
+               $this->assertSame( 'de_DE', $ginger_mo->get_locale() );
+               $this->assertTrue( $ginger_mo->load_file( DIR_TESTDATA . '/l10n/example-simple.mo', 'unittest' ) );
+               $ginger_mo->set_locale( 'es_ES' );
+               $this->assertTrue( $ginger_mo->load_file( DIR_TESTDATA . '/l10n/simple.mo', 'unittest' ) );
+               $ginger_mo->set_locale( 'pl_PL' );
+               $this->assertTrue( $ginger_mo->load_file( DIR_TESTDATA . '/l10n/plural.mo', 'unittest' ) );
+               $this->assertSame( 'pl_PL', $ginger_mo->get_locale() );
+
+               $this->assertTrue( $ginger_mo->is_textdomain_loaded( 'unittest' ) );
+
+               $ginger_mo->set_locale( 'en_US' );
+               $this->assertSame( 'en_US', $ginger_mo->get_locale() );
+
+               $this->assertFalse( $ginger_mo->is_textdomain_loaded( 'unittest' ) );
+               $this->assertTrue( $ginger_mo->is_textdomain_loaded( 'unittest', 'pl_PL' ) );
+               $this->assertTrue( $ginger_mo->is_textdomain_loaded( 'unittest', 'es_ES' ) );
+               $this->assertTrue( $ginger_mo->is_textdomain_loaded( 'unittest', 'de_DE' ) );
+
+               $this->assertTrue( $ginger_mo->unload_textdomain( 'unittest' ) );
+
+               $this->assertFalse( $ginger_mo->is_textdomain_loaded( 'unittest' ) );
+               $this->assertFalse( $ginger_mo->is_textdomain_loaded( 'unittest', 'pl_PL' ) );
+               $this->assertFalse( $ginger_mo->is_textdomain_loaded( 'unittest', 'es_ES' ) );
+               $this->assertFalse( $ginger_mo->is_textdomain_loaded( 'unittest', 'de_DE' ) );
+       }
+
+       /**
+        * @covers ::load
+        * @covers ::locate_translation
+        *
+        * @return void
+        */
+       public function test_load_with_default_textdomain() {
+               $controller = new WP_Translation_Controller();
+               $this->assertTrue( $controller->load_file( DIR_TESTDATA . '/l10n/example-simple.mo' ) );
+               $this->assertTrue( $controller->load_file( DIR_TESTDATA . '/l10n/example-simple.mo' ) );
+               $this->assertFalse( $controller->is_textdomain_loaded( 'unittest' ) );
+               $this->assertSame( 'translation', $controller->translate( 'original' ) );
+       }
+
+       /**
+        * @covers ::load
+        *
+        * @return void
+        */
+       public function test_load_same_file_twice() {
+               $controller = new WP_Translation_Controller();
+               $this->assertTrue( $controller->load_file( DIR_TESTDATA . '/l10n/example-simple.mo', 'unittest' ) );
+               $this->assertTrue( $controller->load_file( DIR_TESTDATA . '/l10n/example-simple.mo', 'unittest' ) );
+
+               $this->assertTrue( $controller->is_textdomain_loaded( 'unittest' ) );
+       }
+
+       /**
+        * @covers ::load
+        *
+        * @return void
+        */
+       public function test_load_file_is_already_loaded_for_different_textdomain() {
+               $controller = new WP_Translation_Controller();
+               $this->assertTrue( $controller->load_file( DIR_TESTDATA . '/l10n/example-simple.mo', 'foo' ) );
+               $this->assertTrue( $controller->load_file( DIR_TESTDATA . '/l10n/example-simple.mo', 'bar' ) );
+
+               $this->assertTrue( $controller->is_textdomain_loaded( 'foo' ) );
+               $this->assertTrue( $controller->is_textdomain_loaded( 'bar' ) );
+       }
+
+       /**
+        * @covers ::load
+        * @covers ::unload
+        * @covers ::is_textdomain_loaded
+        * @covers ::translate
+        * @covers ::translate_plural
+        * @covers ::locate_translation
+        * @covers ::get_files
+        * @covers WP_Translation_File::get_plural_form
+        * @covers WP_Translation_File::make_plural_form_function
+        *
+        * @return void
+        */
+       public function test_load_no_plurals() {
+               $controller = new WP_Translation_Controller();
+               $this->assertTrue( $controller->load_file( DIR_TESTDATA . '/l10n/fa_IR.mo', 'unittest' ) );
+
+               $this->assertTrue( $controller->is_textdomain_loaded( 'unittest' ) );
+
+               $this->assertFalse( $controller->translate( "string that doesn't exist", '', 'unittest' ) );
+
+               $this->assertSame( 'رونوشت‌ها فعال نشدند.', $controller->translate( 'Revisions not enabled.', '', 'unittest' ) );
+               $this->assertSame( 'افزودن جدید', $controller->translate( 'Add New', 'file', 'unittest' ) );
+
+               $this->assertSame( '%s دیدگاه', $controller->translate_plural( array( '%s comment', '%s comments' ), 0, '', 'unittest' ) );
+               $this->assertSame( '%s دیدگاه', $controller->translate_plural( array( '%s comment', '%s comments' ), 1, '', 'unittest' ) );
+               $this->assertSame( '%s دیدگاه', $controller->translate_plural( array( '%s comment', '%s comments' ), 2, '', 'unittest' ) );
+       }
+
+       /**
+        * @covers ::get_headers
+        *
+        * @return void
+        */
+       public function test_get_headers_no_loaded_translations() {
+               $controller = new WP_Translation_Controller();
+               $headers    = $controller->get_headers();
+               $this->assertEmpty( $headers );
+       }
+
+       /**
+        * @covers ::get_headers
+        *
+        * @return void
+        */
+       public function test_get_headers_with_default_textdomain() {
+               $controller = new WP_Translation_Controller();
+               $controller->load_file( DIR_TESTDATA . '/l10n/example-simple.mo' );
+               $headers = $controller->get_headers();
+               $this->assertSame(
+                       array(
+                               'Po-Revision-Date' => '2016-01-05 18:45:32+1000',
+                       ),
+                       $headers
+               );
+       }
+
+       /**
+        * @covers ::get_headers
+        *
+        * @return void
+        */
+       public function test_get_headers_no_loaded_translations_for_domain() {
+               $controller = new WP_Translation_Controller();
+               $controller->load_file( DIR_TESTDATA . '/l10n/example-simple.mo', 'foo' );
+               $headers = $controller->get_headers( 'bar' );
+               $this->assertEmpty( $headers );
+       }
+
+
+       /**
+        * @covers ::get_entries
+        *
+        * @return void
+        */
+       public function test_get_entries_no_loaded_translations() {
+               $controller = new WP_Translation_Controller();
+               $headers    = $controller->get_entries();
+               $this->assertEmpty( $headers );
+       }
+
+       /**
+        * @covers ::get_entries
+        *
+        * @return void
+        */
+       public function test_get_entries_with_default_textdomain() {
+               $controller = new WP_Translation_Controller();
+               $controller->load_file( DIR_TESTDATA . '/l10n/simple.mo' );
+               $headers = $controller->get_entries();
+               $this->assertSame(
+                       array(
+                               'baba'       => 'dyado',
+                               "kuku\nruku" => 'yes',
+                       ),
+                       $headers
+               );
+       }
+
+       /**
+        * @covers ::get_entries
+        *
+        * @return void
+        */
+       public function test_get_entries_no_loaded_translations_for_domain() {
+               $controller = new WP_Translation_Controller();
+               $controller->load_file( DIR_TESTDATA . '/l10n/simple.mo', 'foo' );
+               $headers = $controller->get_entries( 'bar' );
+               $this->assertEmpty( $headers );
+       }
+
+       /**
+        * @dataProvider data_export_matrix
+        *
+        * @param string $source_file
+        * @param string $destination_format
+        * @return void
+        *
+        * @phpstan-param 'mo'|'php' $destination_format
+        */
+       public function test_convert_format( string $source_file, string $destination_format ) {
+               $destination_file = $this->temp_filename();
+
+               $this->assertNotFalse( $destination_file );
+
+               $source = WP_Translation_File::create( $source_file );
+
+               $this->assertInstanceOf( WP_Translation_File::class, $source );
+
+               $contents = WP_Translation_File::transform( $source_file, $destination_format );
+
+               $this->assertNotFalse( $contents );
+
+               file_put_contents( $destination_file, $contents );
+
+               $destination = WP_Translation_File::create( $destination_file, $destination_format );
+
+               $this->assertInstanceOf( WP_Translation_File::class, $destination );
+               $this->assertNull( $destination->error() );
+
+               $this->assertTrue( filesize( $destination_file ) > 0 );
+
+               $destination_read = WP_Translation_File::create( $destination_file, $destination_format );
+
+               $this->assertInstanceOf( WP_Translation_File::class, $destination_read );
+               $this->assertNull( $destination_read->error() );
+
+               $source_headers      = $source->headers();
+               $destination_headers = $destination_read->headers();
+
+               $this->assertEquals( $source_headers, $destination_headers );
+
+               foreach ( $source->entries() as $original => $translation ) {
+                       // Verify the translation is in the destination file
+                       $new_translation = $destination_read->translate( $original );
+                       $this->assertSame( $translation, $new_translation );
+               }
+       }
+
+       /**
+        * @return array<array{0:string, 1: 'mo'|'php'}>
+        */
+       public function data_export_matrix(): array {
+               $formats = array( 'mo', 'php' );
+
+               $matrix = array();
+
+               foreach ( $formats as $input_format ) {
+                       foreach ( $formats as $output_format ) {
+                               $matrix[ "$input_format to $output_format" ] = array( DIR_TESTDATA . '/l10n/example-simple.' . $input_format, $output_format );
+                       }
+               }
+
+               return $matrix;
+       }
+
+       /**
+        * @covers WP_Translation_File::transform
+        *
+        * @return void
+        */
+       public function test_convert_format_invalid_source() {
+               $this->assertFalse( WP_Translation_File::transform( 'this-file-does-not-exist', 'invalid' ) );
+               $this->assertFalse( WP_Translation_File::transform( DIR_TESTDATA . '/l10n/example-simple.mo', 'invalid' ) );
+               $this->assertNotFalse( WP_Translation_File::transform( DIR_TESTDATA . '/l10n/example-simple.mo', 'php' ) );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/l10n/wpTranslationsConvert.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span></div>

</body>
</html>