1<?php
2/**
3 * I18N: WP_Translation_File class.
4 *
5 * @package WordPress
6 * @subpackage I18N
7 * @since 6.5.0
8 */
9
10/**
11 * Class WP_Translation_File.
12 *
13 * @since 6.5.0
14 */
15abstract class WP_Translation_File {
16 /**
17 * List of headers.
18 *
19 * @since 6.5.0
20 * @var array<string, string>
21 */
22 protected $headers = array();
23
24 /**
25 * Whether file has been parsed.
26 *
27 * @since 6.5.0
28 * @var bool
29 */
30 protected $parsed = false;
31
32 /**
33 * Error information.
34 *
35 * @since 6.5.0
36 * @var string|null Error message or null if no error.
37 */
38 protected $error;
39
40 /**
41 * File name.
42 *
43 * @since 6.5.0
44 * @var string
45 */
46 protected $file = '';
47
48 /**
49 * Translation entries.
50 *
51 * @since 6.5.0
52 * @var array<string, string>
53 */
54 protected $entries = array();
55
56 /**
57 * Plural forms function.
58 *
59 * @since 6.5.0
60 * @var callable|null Plural forms.
61 */
62 protected $plural_forms = null;
63
64 /**
65 * Constructor.
66 *
67 * @since 6.5.0
68 *
69 * @param string $file File to load.
70 */
71 protected function __construct( string $file ) {
72 $this->file = $file;
73 }
74
75 /**
76 * Creates a new WP_Translation_File instance for a given file.
77 *
78 * @since 6.5.0
79 *
80 * @param string $file File name.
81 * @param string|null $filetype Optional. File type. Default inferred from file name.
82 * @return false|WP_Translation_File
83 */
84 public static function create( string $file, ?string $filetype = null ) {
85 if ( ! is_readable( $file ) ) {
86 return false;
87 }
88
89 if ( null === $filetype ) {
90 $pos = strrpos( $file, '.' );
91 if ( false !== $pos ) {
92 $filetype = substr( $file, $pos + 1 );
93 }
94 }
95
96 switch ( $filetype ) {
97 case 'mo':
98 return new WP_Translation_File_MO( $file );
99 case 'php':
100 return new WP_Translation_File_PHP( $file );
101 default:
102 return false;
103 }
104 }
105
106 /**
107 * Creates a new WP_Translation_File instance for a given file.
108 *
109 * @since 6.5.0
110 *
111 * @param string $file Source file name.
112 * @param string $filetype Desired target file type.
113 * @return string|false Transformed translation file contents on success, false otherwise.
114 */
115 public static function transform( string $file, string $filetype ) {
116 $source = self::create( $file );
117
118 if ( false === $source ) {
119 return false;
120 }
121
122 switch ( $filetype ) {
123 case 'mo':
124 $destination = new WP_Translation_File_MO( '' );
125 break;
126 case 'php':
127 $destination = new WP_Translation_File_PHP( '' );
128 break;
129 default:
130 return false;
131 }
132
133 $success = $destination->import( $source );
134
135 if ( ! $success ) {
136 return false;
137 }
138
139 return $destination->export();
140 }
141
142 /**
143 * Returns all headers.
144 *
145 * @since 6.5.0
146 *
147 * @return array<string, string> Headers.
148 */
149 public function headers(): array {
150 if ( ! $this->parsed ) {
151 $this->parse_file();
152 }
153 return $this->headers;
154 }
155
156 /**
157 * Returns all entries.
158 *
159 * @since 6.5.0
160 *
161 * @return array<string, string[]> Entries.
162 */
163 public function entries(): array {
164 if ( ! $this->parsed ) {
165 $this->parse_file();
166 }
167
168 return $this->entries;
169 }
170
171 /**
172 * Returns the current error information.
173 *
174 * @since 6.5.0
175 *
176 * @return string|null Error message or null if no error.
177 */
178 public function error() {
179 return $this->error;
180 }
181
182 /**
183 * Returns the file name.
184 *
185 * @since 6.5.0
186 *
187 * @return string File name.
188 */
189 public function get_file(): string {
190 return $this->file;
191 }
192
193 /**
194 * Translates a given string.
195 *
196 * @since 6.5.0
197 *
198 * @param string $text String to translate.
199 * @return false|string Translation(s) on success, false otherwise.
200 */
201 public function translate( string $text ) {
202 if ( ! $this->parsed ) {
203 $this->parse_file();
204 }
205
206 return $this->entries[ $text ] ?? false;
207 }
208
209 /**
210 * Returns the plural form for a given number.
211 *
212 * @since 6.5.0
213 *
214 * @param int $number Count.
215 * @return int Plural form.
216 */
217 public function get_plural_form( int $number ): int {
218 if ( ! $this->parsed ) {
219 $this->parse_file();
220 }
221
222 if ( null === $this->plural_forms && isset( $this->headers['plural-forms'] ) ) {
223 $expression = $this->get_plural_expression_from_header( $this->headers['plural-forms'] );
224 $this->plural_forms = $this->make_plural_form_function( $expression );
225 }
226
227 if ( is_callable( $this->plural_forms ) ) {
228 /**
229 * Plural form.
230 *
231 * @var int $result Plural form.
232 */
233 $result = call_user_func( $this->plural_forms, $number );
234
235 return $result;
236 }
237
238 // Default plural form matches English, only "One" is considered singular.
239 return ( 1 === $number ? 0 : 1 );
240 }
241
242 /**
243 * Returns the plural forms expression as a tuple.
244 *
245 * @since 6.5.0
246 *
247 * @param string $header Plural-Forms header string.
248 * @return string Plural forms expression.
249 */
250 protected function get_plural_expression_from_header( string $header ): string {
251 if ( preg_match( '/^\s*nplurals\s*=\s*(\d+)\s*;\s+plural\s*=\s*(.+)$/', $header, $matches ) ) {
252 return trim( $matches[2] );
253 }
254
255 return 'n != 1';
256 }
257
258 /**
259 * Makes a function, which will return the right translation index, according to the
260 * plural forms header.
261 *
262 * @since 6.5.0
263 *
264 * @param string $expression Plural form expression.
265 * @return callable(int $num): int Plural forms function.
266 */
267 protected function make_plural_form_function( string $expression ): callable {
268 try {
269 $handler = new Plural_Forms( rtrim( $expression, ';' ) );
270 return array( $handler, 'get' );
271 } catch ( Exception $e ) {
272 // Fall back to default plural-form function.
273 return $this->make_plural_form_function( 'n != 1' );
274 }
275 }
276
277 /**
278 * Imports translations from another file.
279 *
280 * @since 6.5.0
281 *
282 * @param WP_Translation_File $source Source file.
283 * @return bool True on success, false otherwise.
284 */
285 protected function import( WP_Translation_File $source ): bool {
286 if ( null !== $source->error() ) {
287 return false;
288 }
289
290 $this->headers = $source->headers();
291 $this->entries = $source->entries();
292 $this->error = $source->error();
293
294 return null === $this->error;
295 }
296
297 /**
298 * Parses the file.
299 *
300 * @since 6.5.0
301 */
302 abstract protected function parse_file();
303
304 /**
305 * Exports translation contents as a string.
306 *
307 * @since 6.5.0
308 *
309 * @return string Translation file contents.
310 */
311 abstract public function export();
312}
313