PluginProbe
Translate and Go multilingual – Automatic AI translation – wpLingua / 2.14.1
Translate and Go multilingual – Automatic AI translation – wpLingua v2.14.1
2.16.7 2.16.6 2.16.5 2.16.4 2.16.3 2.16.2 2.16.1 2.16.0 2.15.2 2.15.1 2.15.0 2.14.3 2.14.2 2.14.1 2.14.0 2.13.1 2.13.0 2.12.3 2.12.2 2.12.1 trunk 1.0.3 1.0.4 1.0.5 1.1.0 All 112 releases
wplingua / inc / i18n-script.php

i18n-script.php in Translate and Go multilingual – Automatic AI translation – wpLingua 2.14.1, at inc/i18n-script.php

381 lines 11.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // If this file is called directly, abort.
4 if ( ! defined( 'WPINC' ) ) {
5 die;
6 }
7
8
9 /**
10 * Load or generate a translation JSON for a registered script.
11 *
12 * Checks whether wpLingua should provide a replacement wp-i18n JSON file for
13 * the given script. If needed, reads the original JS, extracts wp-i18n strings,
14 * builds a translations JSON and writes a cached file that WordPress can use.
15 *
16 * Behavior:
17 * - Skips processing in admin or when the default file is already readable.
18 * - Maps registered scripts that depend on 'wp-i18n' and live under wp-content.
19 * - Returns either the original $file, a generated cached JSON path, or an
20 * empty placeholder file path when no strings are found.
21 *
22 * @param string $file Path to the default translation file provided by WP.
23 * @param string $handle Registered script handle.
24 * @param string $domain Text domain passed by WordPress.
25 * @return string Path to the translation JSON file to use.
26 */
27 function wplng_load_script_translation_file( $file, $handle, $domain ) {
28
29 global $wplng_i18n_scripts;
30
31 /**
32 * If we are in the admin dashboard
33 * or it's not a translated page
34 * or if the translation file was generate by wp, plugin or theme
35 * Use the default file
36 */
37
38 if ( is_admin()
39 || wplng_get_language_website_id() === wplng_get_language_current_id()
40 || empty( $file )
41 || is_readable( $file )
42 ) {
43 return $file;
44 }
45
46 /**
47 * Initialize $wplng_i18n_scripts if not already initialized
48 */
49
50 if ( $wplng_i18n_scripts === null ) {
51
52 $wp_scripts = wp_scripts();
53
54 if ( ! $wp_scripts instanceof WP_Scripts ) {
55 return $file;
56 }
57
58 $wplng_i18n_scripts = array();
59
60 foreach ( $wp_scripts->registered as $handle_temp => $script_object ) {
61
62 if ( in_array( 'wp-i18n', (array) $script_object->deps, true )
63 && wplng_str_contains( $script_object->src, '/wp-content/' )
64 ) {
65
66 $wplng_i18n_scripts[ $handle_temp ] = array(
67 'handle' => $script_object->handle,
68 'textdomain' => $script_object->textdomain,
69 'src' => $script_object->src,
70 );
71
72 }
73 }
74 }
75
76 /**
77 * If we can not associate the current script to a knowed script
78 * (We can not know the script JS file, return)
79 */
80
81 if ( empty( $wplng_i18n_scripts ) || empty( $wplng_i18n_scripts[ $handle ] ) ) {
82 return $file;
83 }
84
85 /**
86 * Check if the replacement translation JSON was already generated by wpLingua
87 */
88
89 $file_nomalized = wp_normalize_path( $file );
90 $dir_cache_script = '/script-i18n';
91
92 $file_cache_relative = str_replace(
93 wp_normalize_path( WP_CONTENT_DIR ),
94 '',
95 $file_nomalized
96 );
97
98 if ( $file_nomalized === $file_cache_relative ) {
99 return $file;
100 }
101
102 $file_cache_absolute = WPLNG_CACHE_DIR . $dir_cache_script . $file_cache_relative;
103
104 if ( is_readable( $file_cache_absolute ) ) {
105 return $file_cache_absolute;
106 }
107
108 /**
109 * Get the original script path and content
110 */
111
112 $script_path = str_replace(
113 content_url(),
114 WP_CONTENT_DIR,
115 $wplng_i18n_scripts[ $handle ]['src']
116 );
117
118 $script_path = wp_normalize_path( $script_path );
119
120 if ( ! is_readable( $script_path ) ) {
121 return $file;
122 }
123
124 // Skip files larger than 2MB to avoid memory issues
125 if ( filesize( $script_path ) > 2 * 1024 * 1024 ) {
126 return $file;
127 }
128
129 $script_content = file_get_contents( $script_path );
130
131 /**
132 * Get texts in script
133 */
134
135 $texts = wplng_i18n_script_extract_strings( $script_content );
136
137 $texts_is_empty = true;
138
139 foreach ( $texts as $texts_by_extraction_methode ) {
140 if ( ! empty( $texts_by_extraction_methode ) ) {
141 $texts_is_empty = false;
142 break;
143 }
144 }
145
146 if ( $texts_is_empty ) {
147
148 wplng_put_cache_file(
149 '/script-i18n' . $file_cache_relative,
150 ''
151 );
152
153 return $file;
154 }
155
156 /**
157 * Make the translations JSON
158 */
159
160 $json_content = wplng_i18n_script_generate_json(
161 $texts,
162 $domain
163 );
164
165 // If JSON generation failed, write empty sentinel and return original file
166 if ( '' === $json_content ) {
167 wplng_put_cache_file(
168 '/script-i18n' . $file_cache_relative,
169 ''
170 );
171 return $file;
172 }
173
174 /**
175 * Generate the wpLingua cached JSON file
176 */
177
178 $file_writing_result = wplng_put_cache_file(
179 '/script-i18n' . $file_cache_relative,
180 $json_content
181 );
182
183 // Return the generated file if writing was successful
184 if ( $file_writing_result !== false ) {
185 return $file_cache_absolute;
186 }
187
188 return $file;
189 }
190
191
192 /**
193 * Extract translatable strings from a JavaScript script.
194 *
195 * Scans minified WP JS for wp-i18n call patterns used in builds like:
196 * (0,x.__)("text")
197 * (0,x._x)("text","context")
198 * (0,x._n)("singular","plural",count)
199 * (0,x._nx)("singular","plural",count,"context")
200 *
201 * The function handles both single and double quoted strings, escaped characters,
202 * optional domain arguments and returns an associative array grouping extracted
203 * items by function name ('__', '_x', '_n', '_nx').
204 *
205 * @param string $script JavaScript source code to scan.
206 * @return array {
207 * Associative array with keys:
208 * '__' => array of [ 'text' => string, 'domain' => ?string ],
209 * '_x' => array of [ 'text' => string, 'context' => string, 'domain' => ?string ],
210 * '_n' => array of [ 'singular' => string, 'plural' => string, 'domain' => ?string ],
211 * '_nx' => array of [ 'singular' => string, 'plural' => string, 'context' => string, 'domain' => ?string ],
212 * }
213 */
214 function wplng_i18n_script_extract_strings( $script ) {
215
216 $results = array(
217 '__' => array(),
218 '_x' => array(),
219 '_n' => array(),
220 '_nx' => array(),
221 );
222
223 // In WordPress minified scripts, calls look like:
224 // (0,r.__)("text") or (0,t._x)("text","context")
225 // where r/t/e/n are aliases for wp.i18n
226
227 $patterns = array(
228 // (0,X.__)("text") or (0,X.__)('text') with optional domain
229 '__' => '/\(0,[a-zA-Z_$][a-zA-Z0-9_$]*\.__\)\s*\(\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\1)["\'])*)\1(?:\s*,\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\3)["\'])*)\3)?\s*\)/u',
230
231 // (0,X._x)("text","context") or with single quotes, optional domain
232 '_x' => '/\(0,[a-zA-Z_$][a-zA-Z0-9_$]*\._x\)\s*\(\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\1)["\'])*)\1\s*,\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\3)["\'])*)\3(?:\s*,\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\5)["\'])*)\5)?\s*\)/u',
233
234 // (0,X._n)("singular","plural",number) with optional domain
235 '_n' => '/\(0,[a-zA-Z_$][a-zA-Z0-9_$]*\._n\)\s*\(\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\1)["\'])*)\1\s*,\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\3)["\'])*)\3\s*,\s*[^,)]+(?:\s*,\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\5)["\'])*)\5)?\s*\)/u',
236
237 // (0,X._nx)("singular","plural",number,"context") with optional domain
238 '_nx' => '/\(0,[a-zA-Z_$][a-zA-Z0-9_$]*\._nx\)\s*\(\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\1)["\'])*)\1\s*,\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\3)["\'])*)\3\s*,\s*[^,]+\s*,\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\5)["\'])*)\5(?:\s*,\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\7)["\'])*)\7)?\s*\)/u',
239 );
240
241 // Extraction for __()
242 if ( preg_match_all( $patterns['__'], $script, $matches, PREG_SET_ORDER ) ) {
243 foreach ( $matches as $match ) {
244 $text = wplng_unescape_js_string( $match[2] );
245 if ( ! empty( $text ) ) {
246 $results['__'][] = array(
247 'text' => $text,
248 'domain' => isset( $match[4] ) && $match[4] !== '' ? wplng_unescape_js_string( $match[4] ) : null,
249 );
250 }
251 }
252 }
253
254 // Extraction for _x()
255 if ( preg_match_all( $patterns['_x'], $script, $matches, PREG_SET_ORDER ) ) {
256 foreach ( $matches as $match ) {
257 $text = wplng_unescape_js_string( $match[2] );
258 if ( ! empty( $text ) ) {
259 $results['_x'][] = array(
260 'text' => $text,
261 'context' => wplng_unescape_js_string( $match[4] ),
262 'domain' => isset( $match[6] ) && $match[6] !== '' ? wplng_unescape_js_string( $match[6] ) : null,
263 );
264 }
265 }
266 }
267
268 // Extraction for _n()
269 if ( preg_match_all( $patterns['_n'], $script, $matches, PREG_SET_ORDER ) ) {
270 foreach ( $matches as $match ) {
271 $singular = wplng_unescape_js_string( $match[2] );
272 if ( ! empty( $singular ) ) {
273 $results['_n'][] = array(
274 'singular' => $singular,
275 'plural' => wplng_unescape_js_string( $match[4] ),
276 'domain' => isset( $match[6] ) && $match[6] !== '' ? wplng_unescape_js_string( $match[6] ) : null,
277 );
278 }
279 }
280 }
281
282 // Extraction for _nx()
283 if ( preg_match_all( $patterns['_nx'], $script, $matches, PREG_SET_ORDER ) ) {
284 foreach ( $matches as $match ) {
285 $singular = wplng_unescape_js_string( $match[2] );
286 if ( ! empty( $singular ) ) {
287 $results['_nx'][] = array(
288 'singular' => $singular,
289 'plural' => wplng_unescape_js_string( $match[4] ),
290 'context' => wplng_unescape_js_string( $match[6] ),
291 'domain' => isset( $match[8] ) && $match[8] !== '' ? wplng_unescape_js_string( $match[8] ) : null,
292 );
293 }
294 }
295 }
296
297 return $results;
298 }
299
300
301 /**
302 * Generates a WordPress wp-i18n compatible translation JSON
303 *
304 * @param array $texts The texts extracted by wplng_i18n_script_extract_strings()
305 * @param string $domain The text domain
306 * @param string $locale The locale (e.g., 'fr_FR')
307 * @param string $script_path Path to the JS script (for reference)
308 * @return string Encoded JSON
309 */
310 function wplng_i18n_script_generate_json( $texts, $domain = 'messages' ) {
311
312 $locale = get_locale();
313
314 // Determine the plural form based on locale
315 $plural_forms = wplng_get_language_plural_forms( $locale );
316
317 // Build the messages array
318 $messages = array(
319 '' => array(
320 'domain' => $domain,
321 'plural-forms' => $plural_forms,
322 'lang' => str_replace( '_', '-', $locale ),
323 ),
324 );
325
326 // Add __() translations
327 foreach ( $texts['__'] as $item ) {
328 $key = $item['text'];
329 $text_domain = ! empty( $item['domain'] ) ? $item['domain'] : $domain;
330 // Value = array with the translation
331 $messages[ $key ] = array( __( $item['text'], $text_domain ) );
332 }
333
334 // Add _x() translations with context
335 foreach ( $texts['_x'] as $item ) {
336 // Key with context: "text\u0004context"
337 $key = $item['text'] . "\x04" . $item['context'];
338 $text_domain = ! empty( $item['domain'] ) ? $item['domain'] : $domain;
339 $messages[ $key ] = array( _x( $item['text'], $item['context'], $text_domain ) );
340 }
341
342 // Add _n() translations (plurals)
343 foreach ( $texts['_n'] as $item ) {
344 $key = $item['singular'];
345 $text_domain = ! empty( $item['domain'] ) ? $item['domain'] : $domain;
346 // Array with [translated singular, translated plural]
347 $messages[ $key ] = array(
348 __( $item['singular'], $text_domain ),
349 __( $item['plural'], $text_domain ),
350 );
351 }
352
353 // Add _nx() translations (plurals with context)
354 foreach ( $texts['_nx'] as $item ) {
355 $key = $item['singular'] . "\x04" . $item['context'];
356 $text_domain = ! empty( $item['domain'] ) ? $item['domain'] : $domain;
357 $messages[ $key ] = array(
358 _x( $item['singular'], $item['context'], $text_domain ),
359 _x( $item['plural'], $item['context'], $text_domain ),
360 );
361 }
362
363 // Build the complete structure
364 $json_data = array(
365 'translation-revision-date' => gmdate( 'Y-m-d H:i:s+0000' ),
366 'generator' => 'wpLingua',
367 'domain' => $domain,
368 'locale_data' => array(
369 'messages' => $messages,
370 ),
371 );
372
373 $encoded = wp_json_encode( $json_data, JSON_UNESCAPED_UNICODE );
374
375 if ( false === $encoded ) {
376 return '';
377 }
378
379 return $encoded;
380 }
381