PluginProbe
Translate and Go multilingual – Automatic AI translation – wpLingua / 2.16.7
Translate and Go multilingual – Automatic AI translation – wpLingua v2.16.7
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
← All changes | inc/i18n-script.php +380 -362 2.12.22.16.7 View file →
@@ -1,362 +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 - $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 -}
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 +}