1<?php
2/**
3 * Locale API: WP_Locale_Switcher class
4 *
5 * @package WordPress
6 * @subpackage i18n
7 * @since 4.7.0
8 */
9
10/**
11 * Core class used for switching locales.
12 *
13 * @since 4.7.0
14 */
15#[AllowDynamicProperties]
16class WP_Locale_Switcher {
17 /**
18 * Locale switching stack.
19 *
20 * @since 6.2.0
21 * @var array
22 */
23 private $stack = array();
24
25 /**
26 * Original locale.
27 *
28 * @since 4.7.0
29 * @var string
30 */
31 private $original_locale;
32
33 /**
34 * Holds all available languages.
35 *
36 * @since 4.7.0
37 * @var string[] An array of language codes (file names without the .mo extension).
38 */
39 private $available_languages;
40
41 /**
42 * Constructor.
43 *
44 * Stores the original locale as well as a list of all available languages.
45 *
46 * @since 4.7.0
47 */
48 public function __construct() {
49 $this->original_locale = determine_locale();
50 $this->available_languages = array_merge( array( 'en_US' ), get_available_languages() );
51 }
52
53 /**
54 * Initializes the locale switcher.
55 *
56 * Hooks into the {@see 'locale'} and {@see 'determine_locale'} filters
57 * to change the locale on the fly.
58 *
59 * @since 4.7.0
60 */
61 public function init() {
62 add_filter( 'locale', array( $this, 'filter_locale' ) );
63 add_filter( 'determine_locale', array( $this, 'filter_locale' ) );
64 }
65
66 /**
67 * Switches the translations according to the given locale.
68 *
69 * @since 4.7.0
70 *
71 * @param string $locale The locale to switch to.
72 * @param int|false $user_id Optional. User ID as context. Default false.
73 * @return bool True on success, false on failure.
74 */
75 public function switch_to_locale( $locale, $user_id = false ) {
76 $current_locale = determine_locale();
77 if ( $current_locale === $locale ) {
78 return false;
79 }
80
81 if ( ! in_array( $locale, $this->available_languages, true ) ) {
82 return false;
83 }
84
85 $this->stack[] = array( $locale, $user_id );
86
87 $this->change_locale( $locale );
88
89 /**
90 * Fires when the locale is switched.
91 *
92 * @since 4.7.0
93 * @since 6.2.0 The `$user_id` parameter was added.
94 *
95 * @param string $locale The new locale.
96 * @param false|int $user_id User ID for context if available.
97 */
98 do_action( 'switch_locale', $locale, $user_id );
99
100 return true;
101 }
102
103 /**
104 * Switches the translations according to the given user's locale.
105 *
106 * @since 6.2.0
107 *
108 * @param int $user_id User ID.
109 * @return bool True on success, false on failure.
110 */
111 public function switch_to_user_locale( $user_id ) {
112 $locale = get_user_locale( $user_id );
113 return $this->switch_to_locale( $locale, $user_id );
114 }
115
116 /**
117 * Restores the translations according to the previous locale.
118 *
119 * @since 4.7.0
120 *
121 * @return string|false Locale on success, false on failure.
122 */
123 public function restore_previous_locale() {
124 $previous_locale = array_pop( $this->stack );
125
126 if ( null === $previous_locale ) {
127 // The stack is empty, bail.
128 return false;
129 }
130
131 $entry = end( $this->stack );
132 $locale = is_array( $entry ) ? $entry[0] : false;
133
134 if ( ! $locale ) {
135 // There's nothing left in the stack: go back to the original locale.
136 $locale = $this->original_locale;
137 }
138
139 $this->change_locale( $locale );
140
141 /**
142 * Fires when the locale is restored to the previous one.
143 *
144 * @since 4.7.0
145 *
146 * @param string $locale The new locale.
147 * @param string $previous_locale The previous locale.
148 */
149 do_action( 'restore_previous_locale', $locale, $previous_locale[0] );
150
151 return $locale;
152 }
153
154 /**
155 * Restores the translations according to the original locale.
156 *
157 * @since 4.7.0
158 *
159 * @return string|false Locale on success, false on failure.
160 */
161 public function restore_current_locale() {
162 if ( empty( $this->stack ) ) {
163 return false;
164 }
165
166 $this->stack = array( array( $this->original_locale, false ) );
167
168 return $this->restore_previous_locale();
169 }
170
171 /**
172 * Whether switch_to_locale() is in effect.
173 *
174 * @since 4.7.0
175 *
176 * @return bool True if the locale has been switched, false otherwise.
177 */
178 public function is_switched() {
179 return ! empty( $this->stack );
180 }
181
182 /**
183 * Returns the locale currently switched to.
184 *
185 * @since 6.2.0
186 *
187 * @return string|false Locale if the locale has been switched, false otherwise.
188 */
189 public function get_switched_locale() {
190 $entry = end( $this->stack );
191
192 if ( $entry ) {
193 return $entry[0];
194 }
195
196 return false;
197 }
198
199 /**
200 * Returns the user ID related to the currently switched locale.
201 *
202 * @since 6.2.0
203 *
204 * @return int|false User ID if set and if the locale has been switched, false otherwise.
205 */
206 public function get_switched_user_id() {
207 $entry = end( $this->stack );
208
209 if ( $entry ) {
210 return $entry[1];
211 }
212
213 return false;
214 }
215
216 /**
217 * Filters the locale of the WordPress installation.
218 *
219 * @since 4.7.0
220 *
221 * @param string $locale The locale of the WordPress installation.
222 * @return string The locale currently being switched to.
223 */
224 public function filter_locale( $locale ) {
225 $switched_locale = $this->get_switched_locale();
226
227 if ( $switched_locale ) {
228 return $switched_locale;
229 }
230
231 return $locale;
232 }
233
234 /**
235 * Load translations for a given locale.
236 *
237 * When switching to a locale, translations for this locale must be loaded from scratch.
238 *
239 * @since 4.7.0
240 *
241 * @global Mo[] $l10n An array of all currently loaded text domains.
242 *
243 * @param string $locale The locale to load translations for.
244 */
245 private function load_translations( $locale ) {
246 global $l10n;
247
248 $domains = $l10n ? array_keys( $l10n ) : array();
249
250 load_default_textdomain( $locale );
251
252 foreach ( $domains as $domain ) {
253 // The default text domain is handled by `load_default_textdomain()`.
254 if ( 'default' === $domain ) {
255 continue;
256 }
257
258 /*
259 * Unload current text domain but allow them to be reloaded
260 * after switching back or to another locale.
261 */
262 unload_textdomain( $domain, true );
263 get_translations_for_domain( $domain );
264 }
265 }
266
267 /**
268 * Changes the site's locale to the given one.
269 *
270 * Loads the translations, changes the global `$wp_locale` object and updates
271 * all post type labels.
272 *
273 * @since 4.7.0
274 *
275 * @global WP_Locale $wp_locale WordPress date and time locale object.
276 * @global PHPMailer\PHPMailer\PHPMailer $phpmailer
277 *
278 * @param string $locale The locale to change to.
279 */
280 private function change_locale( $locale ) {
281 global $wp_locale, $phpmailer;
282
283 $this->load_translations( $locale );
284
285 $wp_locale = new WP_Locale();
286
287 WP_Translation_Controller::get_instance()->set_locale( $locale );
288
289 if ( $phpmailer instanceof WP_PHPMailer ) {
290 $phpmailer->setLanguage();
291 }
292
293 /**
294 * Fires when the locale is switched to or restored.
295 *
296 * @since 4.7.0
297 *
298 * @param string $locale The new locale.
299 */
300 do_action( 'change_locale', $locale );
301 }
302}
303