PluginProbe
Translate and Go multilingual – Automatic AI translation – wpLingua / 2.12.3
Translate and Go multilingual – Automatic AI translation – wpLingua v2.12.3
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.12.3, at inc/i18n-script.php

363 lines 10.6 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 $script_content = file_get_contents( $script_path );
125
126 /**
127 * Get texts in script
128 */
129
130 $texts = wplng_i18n_script_extract_strings( $script_content );
131
132 $texts_is_empty = true;
133
134 foreach ( $texts as $texts_by_extraction_methode ) {
135 if ( ! empty( $texts_by_extraction_methode ) ) {
136 $texts_is_empty = false;
137 break;
138 }
139 }
140
141 if ( $texts_is_empty ) {
142
143 // file_put_contents( $file_cache_relative, '' );
144
145 wplng_put_cache_file(
146 '/script-i18n' . $file_cache_relative,
147 ''
148 );
149
150 return $file;
151 }
152
153 /**
154 * Make the translations JSON
155 */
156
157 $json_content = wplng_i18n_script_generate_json(
158 $texts,
159 $domain
160 );
161
162 /**
163 * Generate the wpLingua cached JSON file
164 */
165
166 $file_writing_result = wplng_put_cache_file(
167 '/script-i18n' . $file_cache_relative,
168 $json_content
169 );
170
171 // Return the generated file if writing was successful
172 if ( $file_writing_result !== false ) {
173 return $file_cache_absolute;
174 }
175
176 return $file;
177 }
178
179
180 /**
181 * Extract translatable strings from a JavaScript script.
182 *
183 * Scans minified WP JS for wp-i18n call patterns used in builds like:
184 * (0,x.__)("text")
185 * (0,x._x)("text","context")
186 * (0,x._n)("singular","plural",count)
187 * (0,x._nx)("singular","plural",count,"context")
188 *
189 * The function handles both single and double quoted strings, escaped characters,
190 * optional domain arguments and returns an associative array grouping extracted
191 * items by function name ('__', '_x', '_n', '_nx').
192 *
193 * @param string $script JavaScript source code to scan.
194 * @return array {
195 * Associative array with keys:
196 * '__' => array of [ 'text' => string, 'domain' => ?string ],
197 * '_x' => array of [ 'text' => string, 'context' => string, 'domain' => ?string ],
198 * '_n' => array of [ 'singular' => string, 'plural' => string, 'domain' => ?string ],
199 * '_nx' => array of [ 'singular' => string, 'plural' => string, 'context' => string, 'domain' => ?string ],
200 * }
201 */
202 function wplng_i18n_script_extract_strings( $script ) {
203
204 $results = array(
205 '__' => array(),
206 '_x' => array(),
207 '_n' => array(),
208 '_nx' => array(),
209 );
210
211 // In WordPress minified scripts, calls look like:
212 // (0,r.__)("text") or (0,t._x)("text","context")
213 // where r/t/e/n are aliases for wp.i18n
214
215 $patterns = array(
216 // (0,X.__)("text") or (0,X.__)('text') with optional domain
217 '__' => '/\(0,[a-zA-Z_$][a-zA-Z0-9_$]*\.__\)\s*\(\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\1)["\'])*)\1(?:\s*,\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\3)["\'])*)\3)?\s*\)/u',
218
219 // (0,X._x)("text","context") or with single quotes, optional domain
220 '_x' => '/\(0,[a-zA-Z_$][a-zA-Z0-9_$]*\._x\)\s*\(\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\1)["\'])*)\1\s*,\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\3)["\'])*)\3(?:\s*,\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\5)["\'])*)\5)?\s*\)/u',
221
222 // (0,X._n)("singular","plural",number) with optional domain
223 '_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',
224
225 // (0,X._nx)("singular","plural",number,"context") with optional domain
226 '_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',
227 );
228
229 // Extraction for __()
230 if ( preg_match_all( $patterns['__'], $script, $matches, PREG_SET_ORDER ) ) {
231 foreach ( $matches as $match ) {
232 $text = wplng_unescape_js_string( $match[2] );
233 if ( ! empty( $text ) ) {
234 $results['__'][] = array(
235 'text' => $text,
236 'domain' => isset( $match[4] ) && $match[4] !== '' ? wplng_unescape_js_string( $match[4] ) : null,
237 );
238 }
239 }
240 }
241
242 // Extraction for _x()
243 if ( preg_match_all( $patterns['_x'], $script, $matches, PREG_SET_ORDER ) ) {
244 foreach ( $matches as $match ) {
245 $text = wplng_unescape_js_string( $match[2] );
246 if ( ! empty( $text ) ) {
247 $results['_x'][] = array(
248 'text' => $text,
249 'context' => wplng_unescape_js_string( $match[4] ),
250 'domain' => isset( $match[6] ) && $match[6] !== '' ? wplng_unescape_js_string( $match[6] ) : null,
251 );
252 }
253 }
254 }
255
256 // Extraction for _n()
257 if ( preg_match_all( $patterns['_n'], $script, $matches, PREG_SET_ORDER ) ) {
258 foreach ( $matches as $match ) {
259 $singular = wplng_unescape_js_string( $match[2] );
260 if ( ! empty( $singular ) ) {
261 $results['_n'][] = array(
262 'singular' => $singular,
263 'plural' => wplng_unescape_js_string( $match[4] ),
264 'domain' => isset( $match[6] ) && $match[6] !== '' ? wplng_unescape_js_string( $match[6] ) : null,
265 );
266 }
267 }
268 }
269
270 // Extraction for _nx()
271 if ( preg_match_all( $patterns['_nx'], $script, $matches, PREG_SET_ORDER ) ) {
272 foreach ( $matches as $match ) {
273 $singular = wplng_unescape_js_string( $match[2] );
274 if ( ! empty( $singular ) ) {
275 $results['_nx'][] = array(
276 'singular' => $singular,
277 'plural' => wplng_unescape_js_string( $match[4] ),
278 'context' => wplng_unescape_js_string( $match[6] ),
279 'domain' => isset( $match[8] ) && $match[8] !== '' ? wplng_unescape_js_string( $match[8] ) : null,
280 );
281 }
282 }
283 }
284
285 return $results;
286 }
287
288
289 /**
290 * Generates a WordPress wp-i18n compatible translation JSON
291 *
292 * @param array $texts The texts extracted by wplng_i18n_script_extract_strings()
293 * @param string $domain The text domain
294 * @param string $locale The locale (e.g., 'fr_FR')
295 * @param string $script_path Path to the JS script (for reference)
296 * @return string Encoded JSON
297 */
298 function wplng_i18n_script_generate_json( $texts, $domain = 'messages' ) {
299
300 $locale = get_locale();
301
302 // Determine the plural form based on locale
303 $plural_forms = wplng_get_language_plural_forms( $locale );
304
305 // Build the messages array
306 $messages = array(
307 '' => array(
308 'domain' => $domain,
309 'plural-forms' => $plural_forms,
310 'lang' => str_replace( '_', '-', $locale ),
311 ),
312 );
313
314 // Add __() translations
315 foreach ( $texts['__'] as $item ) {
316 $key = $item['text'];
317 $text_domain = ! empty( $item['domain'] ) ? $item['domain'] : $domain;
318 // Value = array with the translation
319 $messages[ $key ] = array( __( $item['text'], $text_domain ) );
320 }
321
322 // Add _x() translations with context
323 foreach ( $texts['_x'] as $item ) {
324 // Key with context: "text\u0004context"
325 $key = $item['text'] . "\x04" . $item['context'];
326 $text_domain = ! empty( $item['domain'] ) ? $item['domain'] : $domain;
327 $messages[ $key ] = array( _x( $item['text'], $item['context'], $text_domain ) );
328 }
329
330 // Add _n() translations (plurals)
331 foreach ( $texts['_n'] as $item ) {
332 $key = $item['singular'];
333 $text_domain = ! empty( $item['domain'] ) ? $item['domain'] : $domain;
334 // Array with [translated singular, translated plural]
335 $messages[ $key ] = array(
336 __( $item['singular'], $text_domain ),
337 __( $item['plural'], $text_domain ),
338 );
339 }
340
341 // Add _nx() translations (plurals with context)
342 foreach ( $texts['_nx'] as $item ) {
343 $key = $item['singular'] . "\x04" . $item['context'];
344 $text_domain = ! empty( $item['domain'] ) ? $item['domain'] : $domain;
345 $messages[ $key ] = array(
346 _x( $item['singular'], $item['context'], $text_domain ),
347 _x( $item['plural'], $item['context'], $text_domain ),
348 );
349 }
350
351 // Build the complete structure
352 $json_data = array(
353 'translation-revision-date' => gmdate( 'Y-m-d H:i:s+0000' ),
354 'generator' => 'wpLingua',
355 'domain' => $domain,
356 'locale_data' => array(
357 'messages' => $messages,
358 ),
359 );
360
361 return wp_json_encode( $json_data, JSON_UNESCAPED_UNICODE );
362 }
363