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
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
|
<?php
/**
* @file
* The API for comparing project translation status with available translation.
*/
/**
* Load common APIs.
*/
// @todo Combine functions differently in files to avoid unnecessary includes.
// Follow-up issue http://drupal.org/node/1834298
require_once __DIR__ . '/l10n_update.translation.inc';
/**
* Clear the project data table.
*/
function l10n_update_flush_projects() {
db_truncate('l10n_update_project')->execute();
drupal_static_reset('l10n_update_build_projects');
}
/**
* Rebuild project list
*
* @param $refresh
* TRUE: Refresh project list.
*
* @return array
* Array of project objects to be considered for translation update.
*/
function l10n_update_build_projects($refresh = FALSE) {
$projects = &drupal_static(__FUNCTION__, array(), $refresh);
if (empty($projects)) {
module_load_include('inc', 'l10n_update');
// Get the project list based on .info files.
$projects = l10n_update_project_list();
// Mark all previous projects as disabled and store new project data.
db_update('l10n_update_project')
->fields(array(
'status' => 0,
))
->execute();
$default_server = l10n_update_default_translation_server();
if (module_exists('update')) {
$projects_info = update_get_available(TRUE);
}
foreach ($projects as $name => $data) {
// Force update fetch of project data in cases where Drupal's performance
// optimized approach is missing out on some projects.
// @see http://drupal.org/node/1671570#comment-6216090
if (module_exists('update') && !isset($projects_info[$name])) {
module_load_include('fetch.inc', 'update');
_update_process_fetch_task($data);
$available = _update_get_cached_available_releases();
if (!empty($available[$name])) {
$projects_info[$name] = $available[$name];
}
}
if (isset($projects_info[$name]['releases']) && $projects_info[$name]['project_status'] != 'not-fetched') {
// Find out if a dev version is installed.
if (preg_match("/^[0-9]+\.x-([0-9]+)\..*-dev$/", $data['info']['version'], $matches)) {
// Find a suitable release to use as alternative translation.
foreach ($projects_info[$name]['releases'] as $project_release) {
// The first release with the same major release number which is not
// a dev release is the one. Releases are sorted the most recent first.
if ($project_release['version_major'] == $matches[1] &&
(!isset($project_release['version_extra']) || $project_release['version_extra'] != 'dev')) {
$release = $project_release;
break;
}
}
}
if (!empty($release['version'])) {
$data['info']['version'] = $release['version'];
}
unset($release);
}
// Without Update module we do a best effort fallback. A development
// release will fall back to the corresponding release version.
elseif (!isset($projects_info) && isset($data['info']['version'])) {
if (preg_match('/[^x](\+\d+)?-dev$/', $data['info']['version'])) {
$data['info']['version'] = preg_replace('/(\+\d+)?-dev$/', '', $data['info']['version']);
}
}
$data += array(
'version' => isset($data['info']['version']) ? $data['info']['version'] : '',
'core' => isset($data['info']['core']) ? $data['info']['core'] : DRUPAL_CORE_COMPATIBILITY,
'l10n_path' => isset($data['info']['l10n path']) && $data['info']['l10n path'] ? $data['info']['l10n path'] : $default_server['pattern'],
'status' => 1,
);
$project = (object) $data;
$projects[$name] = $project;
// Create or update the project record.
db_merge('l10n_update_project')
->key(array('name' => $project->name))
->fields(array(
'name' => $project->name,
'project_type' => $project->project_type,
'core' => $project->core,
'version' => $project->version,
'l10n_path' => $project->l10n_path,
'status' => $project->status,
))
->execute();
// Invalidate the cache of translatable projects.
l10n_update_clear_cache_projects();
}
}
return $projects;
}
/**
* Get update module's project list
*
* @return array
*/
function l10n_update_project_list() {
$projects = array();
$disabled = variable_get('l10n_update_check_disabled', 0);
// Unlike update module, this one has no cache
_l10n_update_project_info_list($projects, system_rebuild_module_data(), 'module', $disabled);
_l10n_update_project_info_list($projects, system_rebuild_theme_data(), 'theme', $disabled);
// Allow other modules to alter projects before fetching and comparing.
drupal_alter('l10n_update_projects', $projects);
return $projects;
}
/**
* Populate an array of project data.
*
* Based on _update_process_info_list()
*
* @param $projects
* @param $list
* @param $project_type
* @param $disabled
* TRUE to include disabled projects too
*/
function _l10n_update_project_info_list(&$projects, $list, $project_type, $disabled = FALSE) {
foreach ($list as $file) {
if (!$disabled && empty($file->status)) {
// Skip disabled modules or themes.
continue;
}
// Skip if the .info file is broken.
if (empty($file->info)) {
continue;
}
// If the .info doesn't define the 'project', try to figure it out.
if (!isset($file->info['project'])) {
$file->info['project'] = l10n_update_get_project_name($file);
}
// If the .info defines the 'interface translation project', this value will
// override the 'project' value.
if (isset($file->info['interface translation project'])) {
$file->info['project'] = $file->info['interface translation project'];
}
// If we still don't know the 'project', give up.
if (empty($file->info['project'])) {
continue;
}
// If we don't already know it, grab the change time on the .info file
// itself. Note: we need to use the ctime, not the mtime (modification
// time) since many (all?) tar implementations will go out of their way to
// set the mtime on the files it creates to the timestamps recorded in the
// tarball. We want to see the last time the file was changed on disk,
// which is left alone by tar and correctly set to the time the .info file
// was unpacked.
if (!isset($file->info['_info_file_ctime'])) {
$info_filename = dirname($file->uri) . '/' . $file->name . '.info';
$file->info['_info_file_ctime'] = filectime($info_filename);
}
$project_name = $file->info['project'];
if (!isset($projects[$project_name])) {
// Only process this if we haven't done this project, since a single
// project can have multiple modules or themes.
$projects[$project_name] = array(
'name' => $project_name,
'info' => $file->info,
'datestamp' => isset($file->info['datestamp']) ? $file->info['datestamp'] : 0,
'includes' => array($file->name => isset($file->info['name']) ? $file->info['name'] : $file->name),
'project_type' => $project_name == 'drupal' ? 'core' : $project_type,
);
}
else {
$projects[$project_name]['includes'][$file->name] = $file->info['name'];
$projects[$project_name]['info']['_info_file_ctime'] = max($projects[$project_name]['info']['_info_file_ctime'], $file->info['_info_file_ctime']);
}
}
}
/**
* Given a $file object (as returned by system_rebuild_module_data()), figure
* out what project it belongs to.
*
* Based on update_get_project_name().
*
* @param $file
* @return string
* @see system_get_files_database()
*/
function l10n_update_get_project_name($file) {
$project_name = '';
if (isset($file->info['project'])) {
$project_name = $file->info['project'];
}
elseif (isset($file->info['package']) && (strpos($file->info['package'], 'Core') === 0)) {
$project_name = 'drupal';
}
return $project_name;
}
/**
* Retrieve data for default server.
*
* @return array
* Array of server parameters:
* - "server_pattern": URI containing po file pattern.
*/
function l10n_update_default_translation_server() {
$pattern = variable_get('l10n_update_default_update_url', L10N_UPDATE_DEFAULT_SERVER_PATTERN);
return array(
'pattern' => $pattern,
);
}
/**
* Check for the latest release of project translations.
*
* @param array $projects
* Array of project names to check. Defaults to all translatable projects.
* @param string $langcodes
* Array of language codes. Defaults to all translatable languages.
*
* @return array
* Available sources indexed by project and language.
*/
// @todo Return batch or NULL
function l10n_update_check_projects($projects = array(), $langcodes = array()) {
if (l10n_update_use_remote_source()) {
// Retrieve the status of both remote and local translation sources by
// using a batch process.
l10n_update_check_projects_batch($projects, $langcodes);
}
else {
// Retrieve and save the status of local translations only.
l10n_update_check_projects_local($projects, $langcodes);
variable_set('l10n_update_last_check', REQUEST_TIME);
}
}
/**
* Gets and stores the status and timestamp of remote po files.
*
* A batch process is used to check for po files at remote locations and (when
* configured) to check for po files in the local file system. The most recent
* translation source states are stored in the state variable
* 'l10n_update_translation_status'.
*
* @param array $projects
* Array of project names to check. Defaults to all translatable projects.
* @param string $langcodes
* Array of language codes. Defaults to all translatable languages.
*/
function l10n_update_check_projects_batch($projects = array(), $langcodes = array()) {
// Build and set the batch process.
$batch = l10n_update_batch_status_build($projects, $langcodes);
batch_set($batch);
}
/**
* Builds a batch to get the status of remote and local translation files.
*
* The batch process fetches the state of both local and (if configured) remote
* translation files. The data of the most recent translation is stored per
* per project and per language. This data is stored in a state variable
* 'l10n_update_translation_status'. The timestamp it was last updated is stored
* in the state variable 'l10n_upate_last_checked'.
*
* @param array $projects
* Array of project names for which to check the state of translation files.
* Defaults to all translatable projects.
* @param array $langcodes
* Array of language codes. Defaults to all translatable languages.
*
* @return array
* Batch definition array.
*/
function l10n_update_batch_status_build($projects = array(), $langcodes = array()) {
$projects = $projects ? $projects : array_keys(l10n_update_get_projects());
$langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
$options = _l10n_update_default_update_options();
$operations = _l10n_update_batch_status_operations($projects, $langcodes, $options);
$batch = array(
'operations' => $operations,
'title' => t('Checking translations'),
'progress_message' => '',
'finished' => 'l10n_update_batch_status_finished',
'error_message' => t('Error checking translation updates.'),
'file' => drupal_get_path('module', 'l10n_update') . '/l10n_update.batch.inc',
);
return $batch;
}
/**
* Helper function to construct batch operations checking remote translation
* status.
*
* @param array $projects
* Array of project names to be processed.
* @param array $langcodes
* Array of language codes.
* @param array $options
* Batch processing options.
*
* @return array
* Array of batch operations.
*/
function _l10n_update_batch_status_operations($projects, $langcodes, $options = array()) {
$operations = array();
foreach ($projects as $project) {
foreach ($langcodes as $langcode) {
// Check status of local and remote translation sources.
$operations[] = array('l10n_update_batch_status_check', array($project, $langcode, $options));
}
}
return $operations;
}
/**
* Check and store the status and timestamp of local po files.
*
* Only po files in the local file system are checked. Any remote translation
* files will be ignored.
*
* Projects may contain a server_pattern option containing a pattern of the
* path to the po source files. If no server_pattern is defined the default
* translation directory is checked for the po file. When a server_pattern is
* defined the specified location is checked. The server_pattern can be set in
* the module's .info.yml file or by using
* hook_l10n_update_projects_alter().
*
* @param array $projects
* Array of project names for which to check the state of translation files.
* Defaults to all translatable projects.
* @param array $langcodes
* Array of language codes. Defaults to all translatable languages.
*/
function l10n_update_check_projects_local($projects = array(), $langcodes = array()) {
$projects = l10n_update_get_projects($projects);
$langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
// For each project and each language we check if a local po file is
// available. When found the source object is updated with the appropriate
// type and timestamp of the po file.
foreach ($projects as $name => $project) {
foreach ($langcodes as $langcode) {
$source = l10n_update_source_build($project, $langcode);
if ($file = l10n_update_source_check_file($source)) {
l10n_update_status_save($name, $langcode, L10N_UPDATE_LOCAL, $file);
}
}
}
}
|