Chris@0
|
1 <?php
|
Chris@0
|
2
|
Chris@0
|
3 /**
|
Chris@0
|
4 * @file
|
Chris@0
|
5 * Mass import-export and batch import functionality for Gettext .po files.
|
Chris@0
|
6 */
|
Chris@0
|
7
|
Chris@18
|
8 use Drupal\Core\Url;
|
Chris@18
|
9 use Drupal\Core\File\Exception\FileException;
|
Chris@0
|
10 use Drupal\Core\Language\LanguageInterface;
|
Chris@0
|
11 use Drupal\file\FileInterface;
|
Chris@0
|
12 use Drupal\locale\Gettext;
|
Chris@0
|
13 use Drupal\locale\Locale;
|
Chris@0
|
14
|
Chris@0
|
15 /**
|
Chris@0
|
16 * Prepare a batch to import all translations.
|
Chris@0
|
17 *
|
Chris@0
|
18 * @param array $options
|
Chris@0
|
19 * An array with options that can have the following elements:
|
Chris@0
|
20 * - 'langcode': The language code. Optional, defaults to NULL, which means
|
Chris@0
|
21 * that the language will be detected from the name of the files.
|
Chris@0
|
22 * - 'overwrite_options': Overwrite options array as defined in
|
Chris@0
|
23 * Drupal\locale\PoDatabaseWriter. Optional, defaults to an empty array.
|
Chris@0
|
24 * - 'customized': Flag indicating whether the strings imported from $file
|
Chris@0
|
25 * are customized translations or come from a community source. Use
|
Chris@0
|
26 * LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. Optional, defaults to
|
Chris@0
|
27 * LOCALE_NOT_CUSTOMIZED.
|
Chris@0
|
28 * - 'finish_feedback': Whether or not to give feedback to the user when the
|
Chris@0
|
29 * batch is finished. Optional, defaults to TRUE.
|
Chris@0
|
30 * @param bool $force
|
Chris@0
|
31 * (optional) Import all available files, even if they were imported before.
|
Chris@0
|
32 *
|
Chris@0
|
33 * @return array|bool
|
Chris@0
|
34 * The batch structure, or FALSE if no files are used to build the batch.
|
Chris@0
|
35 *
|
Chris@0
|
36 * @todo
|
Chris@0
|
37 * Integrate with update status to identify projects needed and integrate
|
Chris@0
|
38 * l10n_update functionality to feed in translation files alike.
|
Chris@0
|
39 * See https://www.drupal.org/node/1191488.
|
Chris@0
|
40 */
|
Chris@0
|
41 function locale_translate_batch_import_files(array $options, $force = FALSE) {
|
Chris@0
|
42 $options += [
|
Chris@0
|
43 'overwrite_options' => [],
|
Chris@0
|
44 'customized' => LOCALE_NOT_CUSTOMIZED,
|
Chris@0
|
45 'finish_feedback' => TRUE,
|
Chris@0
|
46 ];
|
Chris@0
|
47
|
Chris@0
|
48 if (!empty($options['langcode'])) {
|
Chris@0
|
49 $langcodes = [$options['langcode']];
|
Chris@0
|
50 }
|
Chris@0
|
51 else {
|
Chris@0
|
52 // If langcode was not provided, make sure to only import files for the
|
Chris@0
|
53 // languages we have added.
|
Chris@0
|
54 $langcodes = array_keys(\Drupal::languageManager()->getLanguages());
|
Chris@0
|
55 }
|
Chris@0
|
56
|
Chris@0
|
57 $files = locale_translate_get_interface_translation_files([], $langcodes);
|
Chris@0
|
58
|
Chris@0
|
59 if (!$force) {
|
Chris@18
|
60 $result = \Drupal::database()->select('locale_file', 'lf')
|
Chris@0
|
61 ->fields('lf', ['langcode', 'uri', 'timestamp'])
|
Chris@0
|
62 ->condition('langcode', $langcodes)
|
Chris@0
|
63 ->execute()
|
Chris@0
|
64 ->fetchAllAssoc('uri');
|
Chris@0
|
65 foreach ($result as $uri => $info) {
|
Chris@0
|
66 if (isset($files[$uri]) && filemtime($uri) <= $info->timestamp) {
|
Chris@0
|
67 // The file is already imported and not changed since the last import.
|
Chris@0
|
68 // Remove it from file list and don't import it again.
|
Chris@0
|
69 unset($files[$uri]);
|
Chris@0
|
70 }
|
Chris@0
|
71 }
|
Chris@0
|
72 }
|
Chris@0
|
73 return locale_translate_batch_build($files, $options);
|
Chris@0
|
74 }
|
Chris@0
|
75
|
Chris@0
|
76 /**
|
Chris@0
|
77 * Get interface translation files present in the translations directory.
|
Chris@0
|
78 *
|
Chris@0
|
79 * @param array $projects
|
Chris@0
|
80 * (optional) Project names from which to get the translation files and
|
Chris@0
|
81 * history. Defaults to all projects.
|
Chris@0
|
82 * @param array $langcodes
|
Chris@0
|
83 * (optional) Language codes from which to get the translation files and
|
Chris@0
|
84 * history. Defaults to all languages.
|
Chris@0
|
85 *
|
Chris@0
|
86 * @return array
|
Chris@0
|
87 * An array of interface translation files keyed by their URI.
|
Chris@0
|
88 */
|
Chris@0
|
89 function locale_translate_get_interface_translation_files(array $projects = [], array $langcodes = []) {
|
Chris@0
|
90 module_load_include('compare.inc', 'locale');
|
Chris@0
|
91 $files = [];
|
Chris@0
|
92 $projects = $projects ? $projects : array_keys(locale_translation_get_projects());
|
Chris@0
|
93 $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
|
Chris@0
|
94
|
Chris@0
|
95 // Scan the translations directory for files matching a name pattern
|
Chris@0
|
96 // containing a project name and language code: {project}.{langcode}.po or
|
Chris@0
|
97 // {project}-{version}.{langcode}.po.
|
Chris@0
|
98 // Only files of known projects and languages will be returned.
|
Chris@0
|
99 $directory = \Drupal::config('locale.settings')->get('translation.path');
|
Chris@0
|
100 $result = file_scan_directory($directory, '![a-z_]+(\-[0-9a-z\.\-\+]+|)\.[^\./]+\.po$!', ['recurse' => FALSE]);
|
Chris@0
|
101
|
Chris@0
|
102 foreach ($result as $file) {
|
Chris@0
|
103 // Update the file object with project name and version from the file name.
|
Chris@0
|
104 $file = locale_translate_file_attach_properties($file);
|
Chris@0
|
105 if (in_array($file->project, $projects)) {
|
Chris@0
|
106 if (in_array($file->langcode, $langcodes)) {
|
Chris@0
|
107 $files[$file->uri] = $file;
|
Chris@0
|
108 }
|
Chris@0
|
109 }
|
Chris@0
|
110 }
|
Chris@0
|
111
|
Chris@0
|
112 return $files;
|
Chris@0
|
113 }
|
Chris@0
|
114
|
Chris@0
|
115 /**
|
Chris@0
|
116 * Build a locale batch from an array of files.
|
Chris@0
|
117 *
|
Chris@0
|
118 * @param array $files
|
Chris@0
|
119 * Array of file objects to import.
|
Chris@0
|
120 * @param array $options
|
Chris@0
|
121 * An array with options that can have the following elements:
|
Chris@0
|
122 * - 'langcode': The language code. Optional, defaults to NULL, which means
|
Chris@0
|
123 * that the language will be detected from the name of the files.
|
Chris@0
|
124 * - 'overwrite_options': Overwrite options array as defined in
|
Chris@0
|
125 * Drupal\locale\PoDatabaseWriter. Optional, defaults to an empty array.
|
Chris@0
|
126 * - 'customized': Flag indicating whether the strings imported from $file
|
Chris@0
|
127 * are customized translations or come from a community source. Use
|
Chris@0
|
128 * LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. Optional, defaults to
|
Chris@0
|
129 * LOCALE_NOT_CUSTOMIZED.
|
Chris@0
|
130 * - 'finish_feedback': Whether or not to give feedback to the user when the
|
Chris@0
|
131 * batch is finished. Optional, defaults to TRUE.
|
Chris@0
|
132 *
|
Chris@0
|
133 * @return array|bool
|
Chris@0
|
134 * A batch structure or FALSE if $files was empty.
|
Chris@0
|
135 */
|
Chris@0
|
136 function locale_translate_batch_build(array $files, array $options) {
|
Chris@0
|
137 $options += [
|
Chris@0
|
138 'overwrite_options' => [],
|
Chris@0
|
139 'customized' => LOCALE_NOT_CUSTOMIZED,
|
Chris@0
|
140 'finish_feedback' => TRUE,
|
Chris@0
|
141 ];
|
Chris@0
|
142 if (count($files)) {
|
Chris@0
|
143 $operations = [];
|
Chris@0
|
144 foreach ($files as $file) {
|
Chris@0
|
145 // We call locale_translate_batch_import for every batch operation.
|
Chris@0
|
146 $operations[] = ['locale_translate_batch_import', [$file, $options]];
|
Chris@0
|
147 }
|
Chris@0
|
148 // Save the translation status of all files.
|
Chris@0
|
149 $operations[] = ['locale_translate_batch_import_save', []];
|
Chris@0
|
150
|
Chris@0
|
151 // Add a final step to refresh JavaScript and configuration strings.
|
Chris@0
|
152 $operations[] = ['locale_translate_batch_refresh', []];
|
Chris@0
|
153
|
Chris@0
|
154 $batch = [
|
Chris@0
|
155 'operations' => $operations,
|
Chris@0
|
156 'title' => t('Importing interface translations'),
|
Chris@0
|
157 'progress_message' => '',
|
Chris@0
|
158 'error_message' => t('Error importing interface translations'),
|
Chris@0
|
159 'file' => drupal_get_path('module', 'locale') . '/locale.bulk.inc',
|
Chris@0
|
160 ];
|
Chris@0
|
161 if ($options['finish_feedback']) {
|
Chris@0
|
162 $batch['finished'] = 'locale_translate_batch_finished';
|
Chris@0
|
163 }
|
Chris@0
|
164 return $batch;
|
Chris@0
|
165 }
|
Chris@0
|
166 return FALSE;
|
Chris@0
|
167 }
|
Chris@0
|
168
|
Chris@0
|
169 /**
|
Chris@0
|
170 * Implements callback_batch_operation().
|
Chris@0
|
171 *
|
Chris@0
|
172 * Perform interface translation import.
|
Chris@0
|
173 *
|
Chris@0
|
174 * @param object $file
|
Chris@0
|
175 * A file object of the gettext file to be imported. The file object must
|
Chris@0
|
176 * contain a language parameter (other than
|
Chris@0
|
177 * LanguageInterface::LANGCODE_NOT_SPECIFIED). This is used as the language of
|
Chris@0
|
178 * the import.
|
Chris@0
|
179 * @param array $options
|
Chris@0
|
180 * An array with options that can have the following elements:
|
Chris@0
|
181 * - 'langcode': The language code.
|
Chris@0
|
182 * - 'overwrite_options': Overwrite options array as defined in
|
Chris@0
|
183 * Drupal\locale\PoDatabaseWriter. Optional, defaults to an empty array.
|
Chris@0
|
184 * - 'customized': Flag indicating whether the strings imported from $file
|
Chris@0
|
185 * are customized translations or come from a community source. Use
|
Chris@0
|
186 * LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. Optional, defaults to
|
Chris@0
|
187 * LOCALE_NOT_CUSTOMIZED.
|
Chris@0
|
188 * - 'message': Alternative message to display during import. Note, this must
|
Chris@0
|
189 * be sanitized text.
|
Chris@0
|
190 * @param array|\ArrayAccess $context
|
Chris@0
|
191 * Contains a list of files imported.
|
Chris@0
|
192 */
|
Chris@0
|
193 function locale_translate_batch_import($file, array $options, &$context) {
|
Chris@0
|
194 // Merge the default values in the $options array.
|
Chris@0
|
195 $options += [
|
Chris@0
|
196 'overwrite_options' => [],
|
Chris@0
|
197 'customized' => LOCALE_NOT_CUSTOMIZED,
|
Chris@0
|
198 ];
|
Chris@0
|
199
|
Chris@0
|
200 if (isset($file->langcode) && $file->langcode != LanguageInterface::LANGCODE_NOT_SPECIFIED) {
|
Chris@0
|
201
|
Chris@0
|
202 try {
|
Chris@0
|
203 if (empty($context['sandbox'])) {
|
Chris@0
|
204 $context['sandbox']['parse_state'] = [
|
Chris@14
|
205 'filesize' => filesize(\Drupal::service('file_system')->realpath($file->uri)),
|
Chris@0
|
206 'chunk_size' => 200,
|
Chris@0
|
207 'seek' => 0,
|
Chris@0
|
208 ];
|
Chris@0
|
209 }
|
Chris@0
|
210 // Update the seek and the number of items in the $options array().
|
Chris@0
|
211 $options['seek'] = $context['sandbox']['parse_state']['seek'];
|
Chris@0
|
212 $options['items'] = $context['sandbox']['parse_state']['chunk_size'];
|
Chris@0
|
213 $report = Gettext::fileToDatabase($file, $options);
|
Chris@0
|
214 // If not yet finished with reading, mark progress based on size and
|
Chris@0
|
215 // position.
|
Chris@0
|
216 if ($report['seek'] < filesize($file->uri)) {
|
Chris@0
|
217
|
Chris@0
|
218 $context['sandbox']['parse_state']['seek'] = $report['seek'];
|
Chris@0
|
219 // Maximize the progress bar at 95% before completion, the batch API
|
Chris@0
|
220 // could trigger the end of the operation before file reading is done,
|
Chris@0
|
221 // because of floating point inaccuracies. See
|
Chris@0
|
222 // https://www.drupal.org/node/1089472.
|
Chris@0
|
223 $context['finished'] = min(0.95, $report['seek'] / filesize($file->uri));
|
Chris@0
|
224 if (isset($options['message'])) {
|
Chris@0
|
225 $context['message'] = t('@message (@percent%).', ['@message' => $options['message'], '@percent' => (int) ($context['finished'] * 100)]);
|
Chris@0
|
226 }
|
Chris@0
|
227 else {
|
Chris@0
|
228 $context['message'] = t('Importing translation file: %filename (@percent%).', ['%filename' => $file->filename, '@percent' => (int) ($context['finished'] * 100)]);
|
Chris@0
|
229 }
|
Chris@0
|
230 }
|
Chris@0
|
231 else {
|
Chris@0
|
232 // We are finished here.
|
Chris@0
|
233 $context['finished'] = 1;
|
Chris@0
|
234
|
Chris@0
|
235 // Store the file data for processing by the next batch operation.
|
Chris@0
|
236 $file->timestamp = filemtime($file->uri);
|
Chris@0
|
237 $context['results']['files'][$file->uri] = $file;
|
Chris@0
|
238 $context['results']['languages'][$file->uri] = $file->langcode;
|
Chris@0
|
239 }
|
Chris@0
|
240
|
Chris@0
|
241 // Add the reported values to the statistics for this file.
|
Chris@0
|
242 // Each import iteration reports statistics in an array. The results of
|
Chris@0
|
243 // each iteration are added and merged here and stored per file.
|
Chris@0
|
244 if (!isset($context['results']['stats']) || !isset($context['results']['stats'][$file->uri])) {
|
Chris@0
|
245 $context['results']['stats'][$file->uri] = [];
|
Chris@0
|
246 }
|
Chris@0
|
247 foreach ($report as $key => $value) {
|
Chris@0
|
248 if (is_numeric($report[$key])) {
|
Chris@0
|
249 if (!isset($context['results']['stats'][$file->uri][$key])) {
|
Chris@0
|
250 $context['results']['stats'][$file->uri][$key] = 0;
|
Chris@0
|
251 }
|
Chris@0
|
252 $context['results']['stats'][$file->uri][$key] += $report[$key];
|
Chris@0
|
253 }
|
Chris@0
|
254 elseif (is_array($value)) {
|
Chris@0
|
255 $context['results']['stats'][$file->uri] += [$key => []];
|
Chris@0
|
256 $context['results']['stats'][$file->uri][$key] = array_merge($context['results']['stats'][$file->uri][$key], $value);
|
Chris@0
|
257 }
|
Chris@0
|
258 }
|
Chris@0
|
259 }
|
Chris@0
|
260 catch (Exception $exception) {
|
Chris@0
|
261 // Import failed. Store the data of the failing file.
|
Chris@0
|
262 $context['results']['failed_files'][] = $file;
|
Chris@0
|
263 \Drupal::logger('locale')->notice('Unable to import translations file: @file', ['@file' => $file->uri]);
|
Chris@0
|
264 }
|
Chris@0
|
265 }
|
Chris@0
|
266 }
|
Chris@0
|
267
|
Chris@0
|
268 /**
|
Chris@0
|
269 * Implements callback_batch_operation().
|
Chris@0
|
270 *
|
Chris@0
|
271 * Save data of imported files.
|
Chris@0
|
272 *
|
Chris@0
|
273 * @param array|\ArrayAccess $context
|
Chris@0
|
274 * Contains a list of imported files.
|
Chris@0
|
275 */
|
Chris@0
|
276 function locale_translate_batch_import_save($context) {
|
Chris@0
|
277 if (isset($context['results']['files'])) {
|
Chris@0
|
278 foreach ($context['results']['files'] as $file) {
|
Chris@0
|
279 // Update the file history if both project and version are known. This
|
Chris@0
|
280 // table is used by the automated translation update function which tracks
|
Chris@0
|
281 // translation status of module and themes in the system. Other
|
Chris@0
|
282 // translation files are not tracked and are therefore not stored in this
|
Chris@0
|
283 // table.
|
Chris@0
|
284 if ($file->project && $file->version) {
|
Chris@0
|
285 $file->last_checked = REQUEST_TIME;
|
Chris@0
|
286 locale_translation_update_file_history($file);
|
Chris@0
|
287 }
|
Chris@0
|
288 }
|
Chris@0
|
289 $context['message'] = t('Translations imported.');
|
Chris@0
|
290 }
|
Chris@0
|
291 }
|
Chris@0
|
292
|
Chris@0
|
293 /**
|
Chris@0
|
294 * Implements callback_batch_operation().
|
Chris@0
|
295 *
|
Chris@0
|
296 * Refreshes translations after importing strings.
|
Chris@0
|
297 *
|
Chris@0
|
298 * @param array|\ArrayAccess $context
|
Chris@0
|
299 * Contains a list of strings updated and information about the progress.
|
Chris@0
|
300 */
|
Chris@0
|
301 function locale_translate_batch_refresh(&$context) {
|
Chris@0
|
302 if (!isset($context['sandbox']['refresh'])) {
|
Chris@0
|
303 $strings = $langcodes = [];
|
Chris@0
|
304 if (isset($context['results']['stats'])) {
|
Chris@0
|
305 // Get list of unique string identifiers and language codes updated.
|
Chris@0
|
306 $langcodes = array_unique(array_values($context['results']['languages']));
|
Chris@0
|
307 foreach ($context['results']['stats'] as $report) {
|
Chris@0
|
308 $strings = array_merge($strings, $report['strings']);
|
Chris@0
|
309 }
|
Chris@0
|
310 }
|
Chris@0
|
311 if ($strings) {
|
Chris@0
|
312 // Initialize multi-step string refresh.
|
Chris@0
|
313 $context['message'] = t('Updating translations for JavaScript and default configuration.');
|
Chris@0
|
314 $context['sandbox']['refresh']['strings'] = array_unique($strings);
|
Chris@0
|
315 $context['sandbox']['refresh']['languages'] = $langcodes;
|
Chris@0
|
316 $context['sandbox']['refresh']['names'] = [];
|
Chris@0
|
317 $context['results']['stats']['config'] = 0;
|
Chris@0
|
318 $context['sandbox']['refresh']['count'] = count($strings);
|
Chris@0
|
319
|
Chris@0
|
320 // We will update strings on later steps.
|
Chris@0
|
321 $context['finished'] = 0;
|
Chris@0
|
322 }
|
Chris@0
|
323 else {
|
Chris@0
|
324 $context['finished'] = 1;
|
Chris@0
|
325 }
|
Chris@0
|
326 }
|
Chris@0
|
327 elseif ($name = array_shift($context['sandbox']['refresh']['names'])) {
|
Chris@0
|
328 // Refresh all languages for one object at a time.
|
Chris@0
|
329 $count = Locale::config()->updateConfigTranslations([$name], $context['sandbox']['refresh']['languages']);
|
Chris@0
|
330 $context['results']['stats']['config'] += $count;
|
Chris@0
|
331 // Inherit finished information from the "parent" string lookup step so
|
Chris@0
|
332 // visual display of status will make sense.
|
Chris@0
|
333 $context['finished'] = $context['sandbox']['refresh']['names_finished'];
|
Chris@0
|
334 $context['message'] = t('Updating default configuration (@percent%).', ['@percent' => (int) ($context['finished'] * 100)]);
|
Chris@0
|
335 }
|
Chris@0
|
336 elseif (!empty($context['sandbox']['refresh']['strings'])) {
|
Chris@0
|
337 // Not perfect but will give some indication of progress.
|
Chris@0
|
338 $context['finished'] = 1 - count($context['sandbox']['refresh']['strings']) / $context['sandbox']['refresh']['count'];
|
Chris@0
|
339 // Pending strings, refresh 100 at a time, get next pack.
|
Chris@0
|
340 $next = array_slice($context['sandbox']['refresh']['strings'], 0, 100);
|
Chris@0
|
341 array_splice($context['sandbox']['refresh']['strings'], 0, count($next));
|
Chris@0
|
342 // Clear cache and force refresh of JavaScript translations.
|
Chris@0
|
343 _locale_refresh_translations($context['sandbox']['refresh']['languages'], $next);
|
Chris@0
|
344 // Check whether we need to refresh configuration objects.
|
Chris@0
|
345 if ($names = Locale::config()->getStringNames($next)) {
|
Chris@0
|
346 $context['sandbox']['refresh']['names_finished'] = $context['finished'];
|
Chris@0
|
347 $context['sandbox']['refresh']['names'] = $names;
|
Chris@0
|
348 }
|
Chris@0
|
349 }
|
Chris@0
|
350 else {
|
Chris@0
|
351 $context['message'] = t('Updated default configuration.');
|
Chris@0
|
352 $context['finished'] = 1;
|
Chris@0
|
353 }
|
Chris@0
|
354 }
|
Chris@0
|
355
|
Chris@0
|
356 /**
|
Chris@0
|
357 * Implements callback_batch_finished().
|
Chris@0
|
358 *
|
Chris@0
|
359 * Finished callback of system page locale import batch.
|
Chris@0
|
360 *
|
Chris@0
|
361 * @param bool $success
|
Chris@0
|
362 * TRUE if batch successfully completed.
|
Chris@0
|
363 * @param array $results
|
Chris@0
|
364 * Batch results.
|
Chris@0
|
365 */
|
Chris@0
|
366 function locale_translate_batch_finished($success, array $results) {
|
Chris@0
|
367 $logger = \Drupal::logger('locale');
|
Chris@0
|
368 if ($success) {
|
Chris@0
|
369 $additions = $updates = $deletes = $skips = $config = 0;
|
Chris@0
|
370 if (isset($results['failed_files'])) {
|
Chris@0
|
371 if (\Drupal::moduleHandler()->moduleExists('dblog') && \Drupal::currentUser()->hasPermission('access site reports')) {
|
Chris@18
|
372 $message = \Drupal::translation()->formatPlural(count($results['failed_files']), 'One translation file could not be imported. <a href=":url">See the log</a> for details.', '@count translation files could not be imported. <a href=":url">See the log</a> for details.', [':url' => Url::fromRoute('dblog.overview')->toString()]);
|
Chris@0
|
373 }
|
Chris@0
|
374 else {
|
Chris@0
|
375 $message = \Drupal::translation()->formatPlural(count($results['failed_files']), 'One translation file could not be imported. See the log for details.', '@count translation files could not be imported. See the log for details.');
|
Chris@0
|
376 }
|
Chris@17
|
377 \Drupal::messenger()->addError($message);
|
Chris@0
|
378 }
|
Chris@0
|
379 if (isset($results['files'])) {
|
Chris@0
|
380 $skipped_files = [];
|
Chris@0
|
381 // If there are no results and/or no stats (eg. coping with an empty .po
|
Chris@0
|
382 // file), simply do nothing.
|
Chris@0
|
383 if ($results && isset($results['stats'])) {
|
Chris@0
|
384 foreach ($results['stats'] as $filepath => $report) {
|
Chris@0
|
385 $additions += $report['additions'];
|
Chris@0
|
386 $updates += $report['updates'];
|
Chris@0
|
387 $deletes += $report['deletes'];
|
Chris@0
|
388 $skips += $report['skips'];
|
Chris@0
|
389 if ($report['skips'] > 0) {
|
Chris@0
|
390 $skipped_files[] = $filepath;
|
Chris@0
|
391 }
|
Chris@0
|
392 }
|
Chris@0
|
393 }
|
Chris@17
|
394 \Drupal::messenger()->addStatus(\Drupal::translation()->formatPlural(count($results['files']),
|
Chris@0
|
395 'One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.',
|
Chris@0
|
396 '@count translation files imported. %number translations were added, %update translations were updated and %delete translations were removed.',
|
Chris@0
|
397 ['%number' => $additions, '%update' => $updates, '%delete' => $deletes]
|
Chris@0
|
398 ));
|
Chris@0
|
399 $logger->notice('Translations imported: %number added, %update updated, %delete removed.', ['%number' => $additions, '%update' => $updates, '%delete' => $deletes]);
|
Chris@0
|
400
|
Chris@0
|
401 if ($skips) {
|
Chris@0
|
402 if (\Drupal::moduleHandler()->moduleExists('dblog') && \Drupal::currentUser()->hasPermission('access site reports')) {
|
Chris@18
|
403 $message = \Drupal::translation()->formatPlural($skips, 'One translation string was skipped because of disallowed or malformed HTML. <a href=":url">See the log</a> for details.', '@count translation strings were skipped because of disallowed or malformed HTML. <a href=":url">See the log</a> for details.', [':url' => Url::fromRoute('dblog.overview')->toString()]);
|
Chris@0
|
404 }
|
Chris@0
|
405 else {
|
Chris@0
|
406 $message = \Drupal::translation()->formatPlural($skips, 'One translation string was skipped because of disallowed or malformed HTML. See the log for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.');
|
Chris@0
|
407 }
|
Chris@17
|
408 \Drupal::messenger()->addWarning($message);
|
Chris@0
|
409 $logger->warning('@count disallowed HTML string(s) in files: @files.', ['@count' => $skips, '@files' => implode(',', $skipped_files)]);
|
Chris@0
|
410 }
|
Chris@0
|
411 }
|
Chris@0
|
412 }
|
Chris@0
|
413 // Add messages for configuration too.
|
Chris@0
|
414 if (isset($results['stats']['config'])) {
|
Chris@0
|
415 locale_config_batch_finished($success, $results);
|
Chris@0
|
416 }
|
Chris@0
|
417 }
|
Chris@0
|
418
|
Chris@0
|
419 /**
|
Chris@0
|
420 * Creates a file object and populates the timestamp property.
|
Chris@0
|
421 *
|
Chris@0
|
422 * @param string $filepath
|
Chris@0
|
423 * The filepath of a file to import.
|
Chris@0
|
424 *
|
Chris@0
|
425 * @return object
|
Chris@0
|
426 * An object representing the file.
|
Chris@0
|
427 */
|
Chris@0
|
428 function locale_translate_file_create($filepath) {
|
Chris@0
|
429 $file = new stdClass();
|
Chris@18
|
430 $file->filename = \Drupal::service('file_system')->basename($filepath);
|
Chris@0
|
431 $file->uri = $filepath;
|
Chris@0
|
432 $file->timestamp = filemtime($file->uri);
|
Chris@0
|
433 return $file;
|
Chris@0
|
434 }
|
Chris@0
|
435
|
Chris@0
|
436 /**
|
Chris@0
|
437 * Generates file properties from filename and options.
|
Chris@0
|
438 *
|
Chris@0
|
439 * An attempt is made to determine the translation language, project name and
|
Chris@0
|
440 * project version from the file name. Supported file name patterns are:
|
Chris@0
|
441 * {project}-{version}.{langcode}.po, {prefix}.{langcode}.po or {langcode}.po.
|
Chris@0
|
442 * Alternatively the translation language can be set using the $options.
|
Chris@0
|
443 *
|
Chris@0
|
444 * @param object $file
|
Chris@0
|
445 * A file object of the gettext file to be imported.
|
Chris@0
|
446 * @param array $options
|
Chris@0
|
447 * An array with options:
|
Chris@0
|
448 * - 'langcode': The language code. Overrides the file language.
|
Chris@0
|
449 *
|
Chris@0
|
450 * @return object
|
Chris@0
|
451 * Modified file object.
|
Chris@0
|
452 */
|
Chris@0
|
453 function locale_translate_file_attach_properties($file, array $options = []) {
|
Chris@0
|
454 // If $file is a file entity, convert it to a stdClass.
|
Chris@0
|
455 if ($file instanceof FileInterface) {
|
Chris@0
|
456 $file = (object) [
|
Chris@0
|
457 'filename' => $file->getFilename(),
|
Chris@0
|
458 'uri' => $file->getFileUri(),
|
Chris@0
|
459 ];
|
Chris@0
|
460 }
|
Chris@0
|
461
|
Chris@0
|
462 // Extract project, version and language code from the file name. Supported:
|
Chris@0
|
463 // {project}-{version}.{langcode}.po, {prefix}.{langcode}.po or {langcode}.po
|
Chris@0
|
464 preg_match('!
|
Chris@0
|
465 ( # project OR project and version OR empty (group 1)
|
Chris@0
|
466 ([a-z_]+) # project name (group 2)
|
Chris@0
|
467 \. # .
|
Chris@0
|
468 | # OR
|
Chris@0
|
469 ([a-z_]+) # project name (group 3)
|
Chris@0
|
470 \- # -
|
Chris@0
|
471 ([0-9a-z\.\-\+]+) # version (group 4)
|
Chris@0
|
472 \. # .
|
Chris@0
|
473 | # OR
|
Chris@0
|
474 ) # (empty)
|
Chris@0
|
475 ([^\./]+) # language code (group 5)
|
Chris@0
|
476 \. # .
|
Chris@0
|
477 po # po extension
|
Chris@0
|
478 $!x', $file->filename, $matches);
|
Chris@0
|
479 if (isset($matches[5])) {
|
Chris@0
|
480 $file->project = $matches[2] . $matches[3];
|
Chris@0
|
481 $file->version = $matches[4];
|
Chris@0
|
482 $file->langcode = isset($options['langcode']) ? $options['langcode'] : $matches[5];
|
Chris@0
|
483 }
|
Chris@0
|
484 else {
|
Chris@0
|
485 $file->langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED;
|
Chris@0
|
486 }
|
Chris@0
|
487 return $file;
|
Chris@0
|
488 }
|
Chris@0
|
489
|
Chris@0
|
490 /**
|
Chris@0
|
491 * Deletes interface translation files and translation history records.
|
Chris@0
|
492 *
|
Chris@0
|
493 * @param array $projects
|
Chris@0
|
494 * (optional) Project names from which to delete the translation files and
|
Chris@0
|
495 * history. Defaults to all projects.
|
Chris@0
|
496 * @param array $langcodes
|
Chris@0
|
497 * (optional) Language codes from which to delete the translation files and
|
Chris@0
|
498 * history. Defaults to all languages.
|
Chris@0
|
499 *
|
Chris@0
|
500 * @return bool
|
Chris@0
|
501 * TRUE if files are removed successfully. FALSE if one or more files could
|
Chris@0
|
502 * not be deleted.
|
Chris@0
|
503 */
|
Chris@0
|
504 function locale_translate_delete_translation_files(array $projects = [], array $langcodes = []) {
|
Chris@0
|
505 $fail = FALSE;
|
Chris@0
|
506 locale_translation_file_history_delete($projects, $langcodes);
|
Chris@0
|
507
|
Chris@0
|
508 // Delete all translation files from the translations directory.
|
Chris@0
|
509 if ($files = locale_translate_get_interface_translation_files($projects, $langcodes)) {
|
Chris@0
|
510 foreach ($files as $file) {
|
Chris@18
|
511 try {
|
Chris@18
|
512 \Drupal::service('file_system')->delete($file->uri);
|
Chris@18
|
513 }
|
Chris@18
|
514 catch (FileException $e) {
|
Chris@0
|
515 $fail = TRUE;
|
Chris@0
|
516 }
|
Chris@0
|
517 }
|
Chris@0
|
518 }
|
Chris@0
|
519 return !$fail;
|
Chris@0
|
520 }
|
Chris@0
|
521
|
Chris@0
|
522 /**
|
Chris@0
|
523 * Builds a locale batch to refresh configuration.
|
Chris@0
|
524 *
|
Chris@0
|
525 * @param array $options
|
Chris@0
|
526 * An array with options that can have the following elements:
|
Chris@0
|
527 * - 'finish_feedback': (optional) Whether or not to give feedback to the user
|
Chris@0
|
528 * when the batch is finished. Defaults to TRUE.
|
Chris@0
|
529 * @param array $langcodes
|
Chris@0
|
530 * (optional) Array of language codes. Defaults to all translatable languages.
|
Chris@0
|
531 * @param array $components
|
Chris@0
|
532 * (optional) Array of component lists indexed by type. If not present or it
|
Chris@0
|
533 * is an empty array, it will update all components.
|
Chris@0
|
534 *
|
Chris@0
|
535 * @return array
|
Chris@0
|
536 * The batch definition.
|
Chris@0
|
537 */
|
Chris@0
|
538 function locale_config_batch_update_components(array $options, array $langcodes = [], array $components = []) {
|
Chris@0
|
539 $langcodes = $langcodes ? $langcodes : array_keys(\Drupal::languageManager()->getLanguages());
|
Chris@0
|
540 if ($langcodes && $names = Locale::config()->getComponentNames($components)) {
|
Chris@0
|
541 return locale_config_batch_build($names, $langcodes, $options);
|
Chris@0
|
542 }
|
Chris@0
|
543 }
|
Chris@0
|
544
|
Chris@0
|
545 /**
|
Chris@0
|
546 * Creates a locale batch to refresh specific configuration.
|
Chris@0
|
547 *
|
Chris@0
|
548 * @param array $names
|
Chris@0
|
549 * List of configuration object names (which are strings) to update.
|
Chris@0
|
550 * @param array $langcodes
|
Chris@0
|
551 * List of language codes to refresh.
|
Chris@0
|
552 * @param array $options
|
Chris@0
|
553 * (optional) An array with options that can have the following elements:
|
Chris@0
|
554 * - 'finish_feedback': Whether or not to give feedback to the user when the
|
Chris@0
|
555 * batch is finished. Defaults to TRUE.
|
Chris@0
|
556 *
|
Chris@0
|
557 * @return array
|
Chris@0
|
558 * The batch definition.
|
Chris@0
|
559 *
|
Chris@0
|
560 * @see locale_config_batch_refresh_name()
|
Chris@0
|
561 */
|
Chris@0
|
562 function locale_config_batch_build(array $names, array $langcodes, array $options = []) {
|
Chris@0
|
563 $options += ['finish_feedback' => TRUE];
|
Chris@0
|
564 $i = 0;
|
Chris@0
|
565 $batch_names = [];
|
Chris@0
|
566 $operations = [];
|
Chris@0
|
567 foreach ($names as $name) {
|
Chris@0
|
568 $batch_names[] = $name;
|
Chris@0
|
569 $i++;
|
Chris@0
|
570 // During installation the caching of configuration objects is disabled so
|
Chris@0
|
571 // it is very expensive to initialize the \Drupal::config() object on each
|
Chris@0
|
572 // request. We batch a small number of configuration object upgrades
|
Chris@0
|
573 // together to improve the overall performance of the process.
|
Chris@0
|
574 if ($i % 20 == 0) {
|
Chris@0
|
575 $operations[] = ['locale_config_batch_refresh_name', [$batch_names, $langcodes]];
|
Chris@0
|
576 $batch_names = [];
|
Chris@0
|
577 }
|
Chris@0
|
578 }
|
Chris@0
|
579 if (!empty($batch_names)) {
|
Chris@0
|
580 $operations[] = ['locale_config_batch_refresh_name', [$batch_names, $langcodes]];
|
Chris@0
|
581 }
|
Chris@0
|
582 $batch = [
|
Chris@0
|
583 'operations' => $operations,
|
Chris@0
|
584 'title' => t('Updating configuration translations'),
|
Chris@0
|
585 'init_message' => t('Starting configuration update'),
|
Chris@0
|
586 'error_message' => t('Error updating configuration translations'),
|
Chris@0
|
587 'file' => drupal_get_path('module', 'locale') . '/locale.bulk.inc',
|
Chris@0
|
588 ];
|
Chris@0
|
589 if (!empty($options['finish_feedback'])) {
|
Chris@0
|
590 $batch['completed'] = 'locale_config_batch_finished';
|
Chris@0
|
591 }
|
Chris@0
|
592 return $batch;
|
Chris@0
|
593 }
|
Chris@0
|
594
|
Chris@0
|
595 /**
|
Chris@0
|
596 * Implements callback_batch_operation().
|
Chris@0
|
597 *
|
Chris@0
|
598 * Performs configuration translation refresh.
|
Chris@0
|
599 *
|
Chris@0
|
600 * @param array $names
|
Chris@0
|
601 * An array of names of configuration objects to update.
|
Chris@0
|
602 * @param array $langcodes
|
Chris@0
|
603 * (optional) Array of language codes to update. Defaults to all languages.
|
Chris@0
|
604 * @param array|\ArrayAccess $context
|
Chris@0
|
605 * Contains a list of files imported.
|
Chris@0
|
606 *
|
Chris@0
|
607 * @see locale_config_batch_build()
|
Chris@0
|
608 */
|
Chris@0
|
609 function locale_config_batch_refresh_name(array $names, array $langcodes, &$context) {
|
Chris@0
|
610 if (!isset($context['result']['stats']['config'])) {
|
Chris@0
|
611 $context['result']['stats']['config'] = 0;
|
Chris@0
|
612 }
|
Chris@0
|
613 $context['result']['stats']['config'] += Locale::config()->updateConfigTranslations($names, $langcodes);
|
Chris@0
|
614 foreach ($names as $name) {
|
Chris@0
|
615 $context['result']['names'][] = $name;
|
Chris@0
|
616 }
|
Chris@0
|
617 $context['result']['langcodes'] = $langcodes;
|
Chris@0
|
618 $context['finished'] = 1;
|
Chris@0
|
619 }
|
Chris@0
|
620
|
Chris@0
|
621 /**
|
Chris@0
|
622 * Implements callback_batch_finished().
|
Chris@0
|
623 *
|
Chris@0
|
624 * Finishes callback of system page locale import batch.
|
Chris@0
|
625 *
|
Chris@0
|
626 * @param bool $success
|
Chris@0
|
627 * Information about the success of the batch import.
|
Chris@0
|
628 * @param array $results
|
Chris@0
|
629 * Information about the results of the batch import.
|
Chris@0
|
630 *
|
Chris@0
|
631 * @see locale_config_batch_build()
|
Chris@0
|
632 */
|
Chris@0
|
633 function locale_config_batch_finished($success, array $results) {
|
Chris@0
|
634 if ($success) {
|
Chris@0
|
635 $configuration = isset($results['stats']['config']) ? $results['stats']['config'] : 0;
|
Chris@0
|
636 if ($configuration) {
|
Chris@17
|
637 \Drupal::messenger()->addStatus(t('The configuration was successfully updated. There are %number configuration objects updated.', ['%number' => $configuration]));
|
Chris@0
|
638 \Drupal::logger('locale')->notice('The configuration was successfully updated. %number configuration objects updated.', ['%number' => $configuration]);
|
Chris@0
|
639 }
|
Chris@0
|
640 else {
|
Chris@17
|
641 \Drupal::messenger()->addStatus(t('No configuration objects have been updated.'));
|
Chris@0
|
642 \Drupal::logger('locale')->warning('No configuration objects have been updated.');
|
Chris@0
|
643 }
|
Chris@0
|
644 }
|
Chris@0
|
645 }
|