PluginProbe
Translate and boost SEO – Automatic AI translation – wpLingua / 2.16.8
Translate and boost SEO – Automatic AI translation – wpLingua v2.16.8
2.16.8 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 All 113 releases
← All changes | inc/i18n-script.php +380 -380 2.16.52.16.8 View file →
@@ -1,380 +1,380 @@
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 -}
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 +}