|
1 <?php |
|
2 /** |
|
3 * I18N: WP_Translation_Controller class. |
|
4 * |
|
5 * @package WordPress |
|
6 * @subpackage I18N |
|
7 * @since 6.5.0 |
|
8 */ |
|
9 |
|
10 /** |
|
11 * Class WP_Translation_Controller. |
|
12 * |
|
13 * @since 6.5.0 |
|
14 */ |
|
15 final class WP_Translation_Controller { |
|
16 /** |
|
17 * Current locale. |
|
18 * |
|
19 * @since 6.5.0 |
|
20 * @var string |
|
21 */ |
|
22 protected $current_locale = 'en_US'; |
|
23 |
|
24 /** |
|
25 * Map of loaded translations per locale and text domain. |
|
26 * |
|
27 * [ Locale => [ Textdomain => [ ..., ... ] ] ] |
|
28 * |
|
29 * @since 6.5.0 |
|
30 * @var array<string, array<string, WP_Translation_File[]>> |
|
31 */ |
|
32 protected $loaded_translations = array(); |
|
33 |
|
34 /** |
|
35 * List of loaded translation files. |
|
36 * |
|
37 * [ Filename => [ Locale => [ Textdomain => WP_Translation_File ] ] ] |
|
38 * |
|
39 * @since 6.5.0 |
|
40 * @var array<string, array<string, array<string, WP_Translation_File|false>>> |
|
41 */ |
|
42 protected $loaded_files = array(); |
|
43 |
|
44 /** |
|
45 * Container for the main instance of the class. |
|
46 * |
|
47 * @since 6.5.0 |
|
48 * @var WP_Translation_Controller|null |
|
49 */ |
|
50 private static $instance = null; |
|
51 |
|
52 /** |
|
53 * Utility method to retrieve the main instance of the class. |
|
54 * |
|
55 * The instance will be created if it does not exist yet. |
|
56 * |
|
57 * @since 6.5.0 |
|
58 * |
|
59 * @return WP_Translation_Controller |
|
60 */ |
|
61 public static function get_instance(): WP_Translation_Controller { |
|
62 if ( null === self::$instance ) { |
|
63 self::$instance = new self(); |
|
64 } |
|
65 |
|
66 return self::$instance; |
|
67 } |
|
68 |
|
69 /** |
|
70 * Returns the current locale. |
|
71 * |
|
72 * @since 6.5.0 |
|
73 * |
|
74 * @return string Locale. |
|
75 */ |
|
76 public function get_locale(): string { |
|
77 return $this->current_locale; |
|
78 } |
|
79 |
|
80 /** |
|
81 * Sets the current locale. |
|
82 * |
|
83 * @since 6.5.0 |
|
84 * |
|
85 * @param string $locale Locale. |
|
86 */ |
|
87 public function set_locale( string $locale ) { |
|
88 $this->current_locale = $locale; |
|
89 } |
|
90 |
|
91 /** |
|
92 * Loads a translation file for a given text domain. |
|
93 * |
|
94 * @since 6.5.0 |
|
95 * |
|
96 * @param string $translation_file Translation file. |
|
97 * @param string $textdomain Optional. Text domain. Default 'default'. |
|
98 * @param string $locale Optional. Locale. Default current locale. |
|
99 * @return bool True on success, false otherwise. |
|
100 */ |
|
101 public function load_file( string $translation_file, string $textdomain = 'default', ?string $locale = null ): bool { |
|
102 if ( null === $locale ) { |
|
103 $locale = $this->current_locale; |
|
104 } |
|
105 |
|
106 $translation_file = realpath( $translation_file ); |
|
107 |
|
108 if ( false === $translation_file ) { |
|
109 return false; |
|
110 } |
|
111 |
|
112 if ( |
|
113 isset( $this->loaded_files[ $translation_file ][ $locale ][ $textdomain ] ) && |
|
114 false !== $this->loaded_files[ $translation_file ][ $locale ][ $textdomain ] |
|
115 ) { |
|
116 return null === $this->loaded_files[ $translation_file ][ $locale ][ $textdomain ]->error(); |
|
117 } |
|
118 |
|
119 if ( |
|
120 isset( $this->loaded_files[ $translation_file ][ $locale ] ) && |
|
121 array() !== $this->loaded_files[ $translation_file ][ $locale ] |
|
122 ) { |
|
123 $moe = reset( $this->loaded_files[ $translation_file ][ $locale ] ); |
|
124 } else { |
|
125 $moe = WP_Translation_File::create( $translation_file ); |
|
126 if ( false === $moe || null !== $moe->error() ) { |
|
127 $moe = false; |
|
128 } |
|
129 } |
|
130 |
|
131 $this->loaded_files[ $translation_file ][ $locale ][ $textdomain ] = $moe; |
|
132 |
|
133 if ( ! $moe instanceof WP_Translation_File ) { |
|
134 return false; |
|
135 } |
|
136 |
|
137 if ( ! isset( $this->loaded_translations[ $locale ][ $textdomain ] ) ) { |
|
138 $this->loaded_translations[ $locale ][ $textdomain ] = array(); |
|
139 } |
|
140 |
|
141 $this->loaded_translations[ $locale ][ $textdomain ][] = $moe; |
|
142 |
|
143 return true; |
|
144 } |
|
145 |
|
146 /** |
|
147 * Unloads a translation file for a given text domain. |
|
148 * |
|
149 * @since 6.5.0 |
|
150 * |
|
151 * @param WP_Translation_File|string $file Translation file instance or file name. |
|
152 * @param string $textdomain Optional. Text domain. Default 'default'. |
|
153 * @param string $locale Optional. Locale. Defaults to all locales. |
|
154 * @return bool True on success, false otherwise. |
|
155 */ |
|
156 public function unload_file( $file, string $textdomain = 'default', ?string $locale = null ): bool { |
|
157 if ( is_string( $file ) ) { |
|
158 $file = realpath( $file ); |
|
159 } |
|
160 |
|
161 if ( null !== $locale ) { |
|
162 if ( isset( $this->loaded_translations[ $locale ][ $textdomain ] ) ) { |
|
163 foreach ( $this->loaded_translations[ $locale ][ $textdomain ] as $i => $moe ) { |
|
164 if ( $file === $moe || $file === $moe->get_file() ) { |
|
165 unset( $this->loaded_translations[ $locale ][ $textdomain ][ $i ] ); |
|
166 unset( $this->loaded_files[ $moe->get_file() ][ $locale ][ $textdomain ] ); |
|
167 return true; |
|
168 } |
|
169 } |
|
170 } |
|
171 |
|
172 return true; |
|
173 } |
|
174 |
|
175 foreach ( $this->loaded_translations as $l => $domains ) { |
|
176 if ( ! isset( $domains[ $textdomain ] ) ) { |
|
177 continue; |
|
178 } |
|
179 |
|
180 foreach ( $domains[ $textdomain ] as $i => $moe ) { |
|
181 if ( $file === $moe || $file === $moe->get_file() ) { |
|
182 unset( $this->loaded_translations[ $l ][ $textdomain ][ $i ] ); |
|
183 unset( $this->loaded_files[ $moe->get_file() ][ $l ][ $textdomain ] ); |
|
184 return true; |
|
185 } |
|
186 } |
|
187 } |
|
188 |
|
189 return false; |
|
190 } |
|
191 |
|
192 /** |
|
193 * Unloads all translation files for a given text domain. |
|
194 * |
|
195 * @since 6.5.0 |
|
196 * |
|
197 * @param string $textdomain Optional. Text domain. Default 'default'. |
|
198 * @param string $locale Optional. Locale. Defaults to all locales. |
|
199 * @return bool True on success, false otherwise. |
|
200 */ |
|
201 public function unload_textdomain( string $textdomain = 'default', ?string $locale = null ): bool { |
|
202 $unloaded = false; |
|
203 |
|
204 if ( null !== $locale ) { |
|
205 if ( isset( $this->loaded_translations[ $locale ][ $textdomain ] ) ) { |
|
206 $unloaded = true; |
|
207 foreach ( $this->loaded_translations[ $locale ][ $textdomain ] as $moe ) { |
|
208 unset( $this->loaded_files[ $moe->get_file() ][ $locale ][ $textdomain ] ); |
|
209 } |
|
210 } |
|
211 |
|
212 unset( $this->loaded_translations[ $locale ][ $textdomain ] ); |
|
213 |
|
214 return $unloaded; |
|
215 } |
|
216 |
|
217 foreach ( $this->loaded_translations as $l => $domains ) { |
|
218 if ( ! isset( $domains[ $textdomain ] ) ) { |
|
219 continue; |
|
220 } |
|
221 |
|
222 $unloaded = true; |
|
223 |
|
224 foreach ( $domains[ $textdomain ] as $moe ) { |
|
225 unset( $this->loaded_files[ $moe->get_file() ][ $l ][ $textdomain ] ); |
|
226 } |
|
227 |
|
228 unset( $this->loaded_translations[ $l ][ $textdomain ] ); |
|
229 } |
|
230 |
|
231 return $unloaded; |
|
232 } |
|
233 |
|
234 /** |
|
235 * Determines whether translations are loaded for a given text domain. |
|
236 * |
|
237 * @since 6.5.0 |
|
238 * |
|
239 * @param string $textdomain Optional. Text domain. Default 'default'. |
|
240 * @param string $locale Optional. Locale. Default current locale. |
|
241 * @return bool True if there are any loaded translations, false otherwise. |
|
242 */ |
|
243 public function is_textdomain_loaded( string $textdomain = 'default', ?string $locale = null ): bool { |
|
244 if ( null === $locale ) { |
|
245 $locale = $this->current_locale; |
|
246 } |
|
247 |
|
248 return isset( $this->loaded_translations[ $locale ][ $textdomain ] ) && |
|
249 array() !== $this->loaded_translations[ $locale ][ $textdomain ]; |
|
250 } |
|
251 |
|
252 /** |
|
253 * Translates a singular string. |
|
254 * |
|
255 * @since 6.5.0 |
|
256 * |
|
257 * @param string $text Text to translate. |
|
258 * @param string $context Optional. Context for the string. Default empty string. |
|
259 * @param string $textdomain Optional. Text domain. Default 'default'. |
|
260 * @param string $locale Optional. Locale. Default current locale. |
|
261 * @return string|false Translation on success, false otherwise. |
|
262 */ |
|
263 public function translate( string $text, string $context = '', string $textdomain = 'default', ?string $locale = null ) { |
|
264 if ( '' !== $context ) { |
|
265 $context .= "\4"; |
|
266 } |
|
267 |
|
268 $translation = $this->locate_translation( "{$context}{$text}", $textdomain, $locale ); |
|
269 |
|
270 if ( false === $translation ) { |
|
271 return false; |
|
272 } |
|
273 |
|
274 return $translation['entries'][0]; |
|
275 } |
|
276 |
|
277 /** |
|
278 * Translates plurals. |
|
279 * |
|
280 * Checks both singular+plural combinations as well as just singulars, |
|
281 * in case the translation file does not store the plural. |
|
282 * |
|
283 * @since 6.5.0 |
|
284 * |
|
285 * @param array $plurals { |
|
286 * Pair of singular and plural translations. |
|
287 * |
|
288 * @type string $0 Singular translation. |
|
289 * @type string $1 Plural translation. |
|
290 * } |
|
291 * @param int $number Number of items. |
|
292 * @param string $context Optional. Context for the string. Default empty string. |
|
293 * @param string $textdomain Optional. Text domain. Default 'default'. |
|
294 * @param string|null $locale Optional. Locale. Default current locale. |
|
295 * @return string|false Translation on success, false otherwise. |
|
296 */ |
|
297 public function translate_plural( array $plurals, int $number, string $context = '', string $textdomain = 'default', ?string $locale = null ) { |
|
298 if ( '' !== $context ) { |
|
299 $context .= "\4"; |
|
300 } |
|
301 |
|
302 $text = implode( "\0", $plurals ); |
|
303 $translation = $this->locate_translation( "{$context}{$text}", $textdomain, $locale ); |
|
304 |
|
305 if ( false === $translation ) { |
|
306 $text = $plurals[0]; |
|
307 $translation = $this->locate_translation( "{$context}{$text}", $textdomain, $locale ); |
|
308 |
|
309 if ( false === $translation ) { |
|
310 return false; |
|
311 } |
|
312 } |
|
313 |
|
314 /** @var WP_Translation_File $source */ |
|
315 $source = $translation['source']; |
|
316 $num = $source->get_plural_form( $number ); |
|
317 |
|
318 // See \Translations::translate_plural(). |
|
319 return $translation['entries'][ $num ] ?? $translation['entries'][0]; |
|
320 } |
|
321 |
|
322 /** |
|
323 * Returns all existing headers for a given text domain. |
|
324 * |
|
325 * @since 6.5.0 |
|
326 * |
|
327 * @param string $textdomain Optional. Text domain. Default 'default'. |
|
328 * @return array<string, string> Headers. |
|
329 */ |
|
330 public function get_headers( string $textdomain = 'default' ): array { |
|
331 if ( array() === $this->loaded_translations ) { |
|
332 return array(); |
|
333 } |
|
334 |
|
335 $headers = array(); |
|
336 |
|
337 foreach ( $this->get_files( $textdomain ) as $moe ) { |
|
338 foreach ( $moe->headers() as $header => $value ) { |
|
339 $headers[ $this->normalize_header( $header ) ] = $value; |
|
340 } |
|
341 } |
|
342 |
|
343 return $headers; |
|
344 } |
|
345 |
|
346 /** |
|
347 * Normalizes header names to be capitalized. |
|
348 * |
|
349 * @since 6.5.0 |
|
350 * |
|
351 * @param string $header Header name. |
|
352 * @return string Normalized header name. |
|
353 */ |
|
354 protected function normalize_header( string $header ): string { |
|
355 $parts = explode( '-', $header ); |
|
356 $parts = array_map( 'ucfirst', $parts ); |
|
357 return implode( '-', $parts ); |
|
358 } |
|
359 |
|
360 /** |
|
361 * Returns all entries for a given text domain. |
|
362 * |
|
363 * @since 6.5.0 |
|
364 * |
|
365 * @param string $textdomain Optional. Text domain. Default 'default'. |
|
366 * @return array<string, string> Entries. |
|
367 */ |
|
368 public function get_entries( string $textdomain = 'default' ): array { |
|
369 if ( array() === $this->loaded_translations ) { |
|
370 return array(); |
|
371 } |
|
372 |
|
373 $entries = array(); |
|
374 |
|
375 foreach ( $this->get_files( $textdomain ) as $moe ) { |
|
376 $entries = array_merge( $entries, $moe->entries() ); |
|
377 } |
|
378 |
|
379 return $entries; |
|
380 } |
|
381 |
|
382 /** |
|
383 * Locates translation for a given string and text domain. |
|
384 * |
|
385 * @since 6.5.0 |
|
386 * |
|
387 * @param string $singular Singular translation. |
|
388 * @param string $textdomain Optional. Text domain. Default 'default'. |
|
389 * @param string $locale Optional. Locale. Default current locale. |
|
390 * @return array{source: WP_Translation_File, entries: string[]}|false { |
|
391 * Translations on success, false otherwise. |
|
392 * |
|
393 * @type WP_Translation_File $source Translation file instance. |
|
394 * @type string[] $entries Array of translation entries. |
|
395 * } |
|
396 */ |
|
397 protected function locate_translation( string $singular, string $textdomain = 'default', ?string $locale = null ) { |
|
398 if ( array() === $this->loaded_translations ) { |
|
399 return false; |
|
400 } |
|
401 |
|
402 // Find the translation in all loaded files for this text domain. |
|
403 foreach ( $this->get_files( $textdomain, $locale ) as $moe ) { |
|
404 $translation = $moe->translate( $singular ); |
|
405 if ( false !== $translation ) { |
|
406 return array( |
|
407 'entries' => explode( "\0", $translation ), |
|
408 'source' => $moe, |
|
409 ); |
|
410 } |
|
411 if ( null !== $moe->error() ) { |
|
412 // Unload this file, something is wrong. |
|
413 $this->unload_file( $moe, $textdomain, $locale ); |
|
414 } |
|
415 } |
|
416 |
|
417 // Nothing could be found. |
|
418 return false; |
|
419 } |
|
420 |
|
421 /** |
|
422 * Returns all translation files for a given text domain. |
|
423 * |
|
424 * @since 6.5.0 |
|
425 * |
|
426 * @param string $textdomain Optional. Text domain. Default 'default'. |
|
427 * @param string $locale Optional. Locale. Default current locale. |
|
428 * @return WP_Translation_File[] List of translation files. |
|
429 */ |
|
430 protected function get_files( string $textdomain = 'default', ?string $locale = null ): array { |
|
431 if ( null === $locale ) { |
|
432 $locale = $this->current_locale; |
|
433 } |
|
434 |
|
435 return $this->loaded_translations[ $locale ][ $textdomain ] ?? array(); |
|
436 } |
|
437 } |