1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
|
<?php /** * Utility for compiling PO data to MO AND JSON files */ class Loco_gettext_Compiler {
/** * @var Loco_api_WordPressFileSystem|null */ private $fs;
/** * Target file group, where we're compiling to * @var Loco_fs_Siblings */ private $files;
/** * Result when files written * @var Loco_fs_FileList */ private $done; /** * @var Loco_mvc_ViewParams */ private $progress;
/** * Construct with primary file (PO) being saved * @param Loco_fs_File $pofile Localised PO file which may or may not exist yet */ public function __construct( Loco_fs_File $pofile ){ $this->files = new Loco_fs_Siblings($pofile); $this->progress = new Loco_mvc_ViewParams( [ 'pobytes' => 0, 'mobytes' => 0, 'numjson' => 0, 'phbytes' => 0, ] ); // Connect compiler to the file system, if writing to disk for real if( ! $pofile instanceof Loco_fs_DummyFile ) { $this->fs = new Loco_api_WordPressFileSystem; } $this->done = new Loco_fs_FileList; }
/** * Write PO, MO and JSON siblings */ public function writeAll( Loco_gettext_Data $po, ?Loco_package_Project $project = null ):Loco_fs_FileList { $this->writePo($po); $this->writeMo($po); if( $project ){ $this->writeJson($project,$po); } return $this->done; }
/** * @return int bytes written to PO file * @throws Loco_error_WriteException */ public function writePo( Loco_gettext_Data $po ):int { $file = $this->files->getSource(); // Perform PO file backup before overwriting an existing PO if( $file->exists() && $this->fs ){ $backups = new Loco_fs_Revisions($file); $backup = $backups->rotate($this->fs); // debug backup creation only under cli or ajax. too noisy printing on screen if( $backup && ( loco_doing_ajax() || 'cli' === PHP_SAPI ) && $backup->exists() ){ Loco_error_AdminNotices::debug( sprintf('Wrote backup: %s -> %s',$file->basename(),$backup->basename() ) ); } } $bytes = $this->writeFile( $file, $po->msgcat() ); $this->progress['pobytes'] = $bytes; return $bytes; }
/** * @return int bytes written to MO file */ public function writeMo( Loco_gettext_Data $po ):int { try { $mofile = $this->files->getBinary(); $bytes = $this->writeFile( $mofile, $po->msgfmt() ); } catch( Exception $e ){ Loco_error_AdminNotices::debug( $e->getMessage() ); Loco_error_AdminNotices::warn( __('PO file saved, but MO file compilation failed','loco-translate') ); $bytes = 0; } $this->progress['mobytes'] = $bytes; // write PHP cache, if WordPress >= 6.5 if( 0 !== $bytes ){ try { $this->progress['phbytes'] = $this->writePhp($po); } catch( Exception $e ){ Loco_error_AdminNotices::debug( $e->getMessage() ); } } return $bytes; }
/** * @return int bytes written to .l10n.php file */ private function writePhp( Loco_gettext_Data $po ):int { $phfile = $this->files->getCache(); if( $phfile && class_exists('WP_Translation_File_PHP',false) ){ return $this->writeFile( $phfile, Loco_gettext_PhpCache::render($po) ); } return 0; }
/** * @param Loco_package_Project $project Translation set, required to resolve script paths * @param Loco_gettext_Data $po PO data to export */ public function writeJson( Loco_package_Project $project, Loco_gettext_Data $po ):Loco_fs_FileList { $domain = $project->getDomain()->getName(); $pofile = $this->files->getSource(); $jsons = new Loco_fs_FileList; // Allow plugins to dictate a single JSON file to hold all script translations for a text domain // authors will additionally have to filter at runtime on load_script_translation_file $path = apply_filters('loco_compile_single_json', '', $pofile->getPath(), $domain ); if( is_string($path) && '' !== $path ){ $refs = $po->splitRefs( $this->getJsExtMap() ); if( array_key_exists('js',$refs) && $refs['js'] instanceof Loco_gettext_Data ){ $jsonfile = new Loco_fs_File($path); $json = $refs['js']->msgjed($domain,'*.js'); try { if( '' !== $json ){ $this->writeFile($jsonfile,$json); $jsons->add($jsonfile); } } catch( Loco_error_WriteException $e ){ Loco_error_AdminNotices::debug( $e->getMessage() ); // translators: %s refers to a JSON file which could not be compiled due to an error Loco_error_AdminNotices::warn( sprintf(__('JSON compilation failed for %s','loco-translate'),$jsonfile->basename()) ); } } } // continue as per default, generating multiple per-script JSON else { $buffer = []; $base_dir = $project->getBundle()->getDirectoryPath(); $extensions = array_keys( $this->getJsExtMap() ); $refsGrep = '\\.(?:'.implode('|',$extensions).')'; /* @var Loco_gettext_Data $fragment */ foreach( $po->exportRefs($refsGrep) as $ref => $fragment ){ $use = null; // Reference could be a js source file, or a minified version. We'll try .min.js first, then .js // Build systems may differ, but WordPress only supports these suffixes. See WP-CLI MakeJsonCommand. if( substr($ref,-7) === '.min.js' ) { $paths = [ $ref, substr($ref,-7).'.js' ]; } else { $paths = [ substr($ref,0,-3).'.min.js', $ref ]; } // Try .js and .min.js paths to check whether deployed script actually exists foreach( $paths as $path ){ // Hook into load_script_textdomain_relative_path like load_script_textdomain() does. $url = $project->getBundle()->getDirectoryUrl().$path; $path = apply_filters( 'load_script_textdomain_relative_path', $path, $url ); if( ! is_string($path) || '' === $path ){ continue; } // by default ignore js file that is not in deployed code $file = new Loco_fs_File($path); $file->normalize($base_dir); if( apply_filters('loco_compile_script_reference',$file->exists(),$path,$domain) ){ $use = $path; break; } } // if neither exists in the bundle, this is a source path that will never be resolved at runtime if( is_null($use) ){ Loco_error_AdminNotices::debug( sprintf('Skipping JSON for %s; script not found in bundle',$ref) ); } // add .js strings to buffer for this json and merge if already present else if( array_key_exists($use,$buffer) ){ $buffer[$use]->concat($fragment); } else { $buffer[$use] = $fragment; } } if( $buffer ){ // write all buffered fragments to their computed JSON paths foreach( $buffer as $ref => $fragment ) { $json = $fragment->msgjed($domain,$ref); if( '' === $json ){ Loco_error_AdminNotices::debug( sprintf('Skipping JSON for %s; no translations',$ref) ); continue; } try { $jsonfile = self::cloneJson($pofile,$ref,$domain); $this->writeFile( $jsonfile, $json ); $jsons->add($jsonfile); } catch( Loco_error_WriteException $e ){ Loco_error_AdminNotices::debug( $e->getMessage() ); // phpcs:ignore -- comment already applied to this string elsewhere Loco_error_AdminNotices::warn( sprintf(__('JSON compilation failed for %s','loco-translate'),$ref)); } } $buffer = null; } } // clean up redundant JSONs including if no JSONs were compiled if( Loco_data_Settings::get()->jed_clean ){ foreach( $this->files->getJsons($domain) as $path ){ $jsonfile = new Loco_fs_File($path); if( ! $jsons->has($jsonfile) ){ try { $jsonfile->unlink(); } catch( Loco_error_WriteException $e ){ Loco_error_AdminNotices::debug('Unable to remove redundant JSON: '.$e->getMessage() ); } } } } $this->progress['numjson'] = $jsons->count(); return $jsons; }
/** * Clone localised file as a WordPress script translation file */ private static function cloneJson( Loco_fs_File $pofile, string $ref, string $domain ):Loco_fs_File { $name = $pofile->filename(); // Theme author PO files have no text domain, but JSON files must always be prefixed if( $domain && 'default' !== $domain && preg_match('/^[a-z]{2,3}(?:_[a-z\\d_]+)?$/i',$name) ){ $name = $domain.'-'.$name; } // Hashable reference is always finally unminified, as per load_script_textdomain() if( '' !== $ref ){ $name .= '-'.self::hashRef($ref); } return $pofile->cloneBasename( $name.'.json' ); }
/** * Hashable reference is always finally unminified, as per load_script_textdomain() * @param string $ref script path relative to plugin base */ private static function hashRef( string $ref ):string { if( substr($ref,-7) === '.min.js' ) { $ref = substr($ref,0,-7).'.js'; } return md5($ref); }
/** * Fetch compilation summary and raise most relevant success message */ public function getSummary():Loco_mvc_ViewParams { $pofile = $this->files->getSource(); // Avoid calling this unless the initial PO save was successful if( ! $this->progress['pobytes'] ){ throw new LogicException('PO not saved'); } // Summary for localised file includes MO+JSONs $mobytes = $this->progress['mobytes']; $numjson = $this->progress['numjson']; if( $mobytes && $numjson ){ Loco_error_AdminNotices::success( __('PO file saved and MO/JSON files compiled','loco-translate') ); } else if( $mobytes ){ Loco_error_AdminNotices::success( __('PO file saved and MO file compiled','loco-translate') ); } else { // translators: Success notice where %s is a file extension, e.g. "PO" Loco_error_AdminNotices::success( sprintf(__('%s file saved','loco-translate'),strtoupper($pofile->extension())) ); } return $this->progress; }
/** * Obtain non-standard JavaScript file extensions. * @return string[] where keys are PCRE safe extensions, all mapped to "js" */ private function getJsExtMap():array { $map = ['js'=>'js','jsx'=>'js']; $exts = Loco_data_Settings::get()->jsx_alias; if( is_array($exts) && $exts ){ $exts = array_map( [__CLASS__,'pregQuote'], $exts); $map = array_fill_keys($exts,'js') + $map; } return $map; }
/** * @internal */ private static function pregQuote( string $value ):string { return preg_quote($value,'/'); }
/** * @param Loco_fs_File $file * @param string $data to write to given file * @return int bytes written */ public function writeFile( Loco_fs_File $file, string $data ):int { if( $this->fs ) { $this->fs->authorizeSave( $file ); } $bytes = $file->putContents($data); if( 0 !== $bytes ){ $this->done->add($file ); } return $bytes; }
}
|