Commit 3d7d601c authored by Riccardo Padovani's avatar Riccardo Padovani

Added l10n_update module

parent c0073e0c
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
Localization Update
-------------------
Automatically download and update your translations by fetching them from
http://localize.drupal.org or any other Localization server.
The l10n update module helps to keep the translation of your drupal core and
contributed modules up to date with the central Drupal translation repository
at http://localize.drupal.org. Alternatively locally stored translation files
can be used as translation source too.
By choice updates are performed automatically or manually. Locally altered
translations can either be respected or ignored.
The l10n update module is developed for:
* Distributions which include their own translations in .po files.
* Site admins who want to update the translation with each new module revision.
* Site builders who want an easy tool to download translations for a site.
* Multi-sites that share one translation source.
Project page: http://drupal.org/project/l10n_update
Support queue: http://drupal.org/project/issues/l10n_update
Installation
------------
Download, unpack the module the usual way.
Enable this module and the Locale module (core).
You need at least one language other than English.
On Administration > Configuration > Regional and language:
Click "Add language"
Pull-down menu: Choose your new language
Then click "Add language"
Drupal is now importing interface translations. This can take a few minutes.
When it's finished, you'll get a confirmation with a summary of all
translation files that have been pulled in.
If required, enable the new language as default language.
Home > Administration > Configuration > Regional and language:
Select your new language as default
Update interface translations
-----------------------------
On Home > Administration > Configuration > Regional and language:
Choose the "Translation updates" tab
Change "Check for updates" to Daily or Weekly instead of the default "Never".
Cron will from now on check for updated translations, and will report the
update status on the status page (Home > Administration > Reports).
To check the translation status and execute updates manually, go to
Administration > Configuration > Regional and language > Translate inteface
Here you see English and your new language.
Choose the "Update" tab
You see a list of all modules and their translation status.
On the bottom of the page, you can manually update using "Update translations".
Use Drush
---------
You can also use drush to update your translations:
drush l10n-update # Update translations.
drush l10n-update-refresh # Refresh available information.
drush l10n-update-status # Show translation status of available project
Summary of administrative pages
-------------------------------
Translations status overview can be found at
Administration > Configuration > Regional and language > Languages > Translation updates
Update configuration settings can be found at
Administration > Configuration > Regional and language > Translate interface > Update
Translating Drupal core, modules and themes
-------------------------------------------
When Drupal core or contributed modules or themes get installed, Drupal core
checks if .po translation files are present and updates the translations with
the strings found in these files. After this, the localization update module
checks the localization server for more recent translations, and updates
the site translations if a more recent version was found.
Note that the translations contained in the project packages may become
obsolete in future releases.
Changes to translations made locally using the site's build in translation
interface (Administer > Site building > Translate interface > Search) and
changes made using the localization client module are marked. Using the
'Update mode' setting 'Edited translations are kept...', locally edited
strings will not be overwritten by translation updates.
NOTE: Only manual changes made AFTER installing Localization Update module
are preserved. To preserve manual changes made prior to installation of
Localization Update module, use the option 'All existing translations are kept...'.
po files, multi site and distributions
--------------------------------------
Multi sites and other installations that share the file system can share
downloaded translation files. The Localization Update module can save these
translations to disk. Other installations can use these saved translations
as their translation source.
All installations that share the same translation files must be configured
with the same 'Store downloaded files' file path e.g. 'sites/all/translations'.
Set the 'Update source' of one installation to 'Local files and remote server'
or 'Remote server only', all other installations are set to
'Local files only' or 'Local files and remote server'.
Translation files are saved with the following file name syntax:
<module name>-<release>.<language code>.po
For example:
masquerade-6.x-1.5.de.po
tac_lite-7.x-1.0-beta1.nl.po
Po files included in distributions should match this syntax too.
Alternative sources of translation
----------------------------------
Each project i.e. modules, themes, etc. can define alternative translation
servers to retrieve the translation updates from.
Include the following definition in the projects .info file:
l10n server = example.com
l10n url = http://example.com/files/translations/l10n_server.xml
The download path pattern is normally defined in the above defined xml file.
You may override this path by adding a third definition in the .info file:
l10n path = http://example.com/files/translations/%core/%project/%project-%release.%language.po
API
---
Using hook_l10n_servers the l10n update module can be extended to use other
translation repositories. Which is usefull for organisations who maintain
their own translation.
Using hook_l10n_update_projects_alter modules can alter or specify the
translation repositories on a per module basis.
See l10n_update.api.php for more information.
Maintainers
-----------
Jose Reyero
Gábor Hojtsy
Erik Stielstra
/* Expand/collapse image for project title */
html.js .l10n-update-wrapper .project-legend {
padding-right: 10px;
}
html.js .l10n-update-wrapper.collapsed .project-legend {
background: url("../images/menu-collapsed-rtl.png") right 50% no-repeat;
}
html.js .l10n-update-wrapper .project-legend a {
margin-right: -10px;
padding-right: 10px;
}
/* Translation update status data */
html.js .l10n-update-wrapper.collapsed .project .version-status {
float: left;
}
.l10n-update .version-status {
float: left;
}
.l10n-update .version-links {
float: left;
}
html.js .l10n-update-wrapper.collapsed .fieldset-wrapper {
display: none;
}
.l10n-update-wrapper .project .version-status {
display: none;
}
/* Expand/collapse image for project title */
html.js .l10n-update-wrapper .project-title {
background: url("../images/menu-expanded.png") left 65% no-repeat;
padding-left: 10px;
}
html.js .l10n-update-wrapper.collapsed .project-title {
background: url("../images/menu-collapsed.png") left 50% no-repeat;
}
html.js .l10n-update-wrapper .project-title a {
margin-left: -10px;
padding-left: 10px;
}
/* Translation update status data */
html.js .l10n-update-wrapper.collapsed .project .version-status {
display: inline;
float: right;
}
.l10n-update .project-server {
margin: 0 10px;
font-size: 90%;
font-weight: normal
}
.l10n-update .version-status {
float: right;
font-size: 90%;
font-weight: normal;
}
.l10n-update .version-links {
float: right;
padding-right: 1em;
}
(function ($) {
Drupal.behaviors.l10nUpdateCollapse = {
attach: function (context, settings) {
$('.l10n-update .l10n-update-wrapper', context).once('l10nupdatecollapse', function () {
var wrapper = $(this);
// Turn the project title into a clickable link.
// Add an event to toggle the content visibiltiy.
var $legend = $('.project-title', this);
var $link = $('<a href="#"></a>')
.prepend($legend.contents())
.appendTo($legend)
.click(function () {
Drupal.toggleFieldset(wrapper);
return false;
});
});
}
};
})(jQuery);
<?php
/**
* @file
* Admin settings and update page.
*/
/**
* Project has a new release available.
*/
define('L10N_UPDATE_NOT_CURRENT', 4);
/**
* Project is up to date.
*/
define('L10N_UPDATE_CURRENT', 5);
/**
* Project's status cannot be checked.
*/
define('L10N_UPDATE_NOT_CHECKED', -1);
/**
* No available update data was found for project.
*/
define('L10N_UPDATE_UNKNOWN', -2);
/**
* There was a failure fetching available update data for this project.
*/
define('L10N_UPDATE_NOT_FETCHED', -3);
// Include l10n_update API
module_load_include('check.inc', 'l10n_update');
// And project api
module_load_include('project.inc', 'l10n_update');
/**
* Page callback: Admin overview page.
*/
function l10n_update_admin_overview() {
// For now we get package information provided by modules.
$projects = l10n_update_get_projects();
$languages = l10n_update_language_list('name');
$build = array();
if ($languages) {
$history = l10n_update_get_history();
$available = l10n_update_available_releases();
$updates = l10n_update_build_updates($history, $available);
$build['project_status'] = array(
'#theme' => 'l10n_update_project_status',
'#projects' => $projects,
'#languages' => $languages,
'#history' => $history,
'#available' => $available,
'#updates' => $updates,
);
$build['admin_import_form'] = drupal_get_form('l10n_update_admin_import_form', $projects, $updates);
}
else {
$build['no_projects'] = array('#markup' => t('No projects or languages to update.'));
}
return $build;
}
/**
* Translation update form.
*
* @todo selectable packages
* @todo check language support in server
* @todo check file update dates
*
* @param $form_state
* Form states array.
* @param $projects
* @todo $projects are not used in the form.
* @param $updates
* Updates to be displayed in the form.
*/
function l10n_update_admin_import_form($form, $form_state, $projects, $updates) {
//module_load_include('inc', 'l10n_update');
// For now we get package information provided by modules
$projects = l10n_update_get_projects();
$languages = l10n_update_language_list('name');
// Absence of projects is an error and only occurs if the database table
// was truncated. In this case we rebuild the project data.
if (!$projects) {
l10n_update_build_projects();
$projects = l10n_update_get_projects();
}
if ($projects && $languages) {
$form['updates'] = array(
'#type' => 'value',
'#value' => $updates,
);
// @todo Only show this language fieldset if we have more than 1 language.
$form['lang'] = array(
'#type' => 'fieldset',
'#title' => t('Languages'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#description' => t('Select one or more languages to download and update. If you select none, all of them will be updated.'),
);
$form['lang']['languages'] = array(
'#type' => 'checkboxes',
'#options' => $languages,
);
$form['mode'] = array(
'#type' => 'radios',
'#title' => t('Update mode'),
'#default_value' => variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP),
'#options' => _l10n_update_admin_import_options(),
);
$form['buttons']['download'] = array(
'#type' => 'submit',
'#value' => t('Update translations'),
);
}
$form['buttons']['refresh'] = array(
'#type' => 'submit',
'#value' => t('Refresh information'),
);
return $form;
}
/**
* Submit handler for Update form.
*
* Handles both submit buttons to update translations and to update the
* form information.
*/
function l10n_update_admin_import_form_submit($form, $form_state) {
$op = isset($form_state['values']['op']) ? $form_state['values']['op'] : '';
$projects = l10n_update_get_projects();
if ($op == t('Update translations')) {
$languages = array_filter($form_state['values']['languages']);
$updates = $form_state['values']['updates'];
$mode = $form_state['values']['mode'];
if ($projects && $updates) {
module_load_include('batch.inc', 'l10n_update');
// Filter out updates in other languages. If no languages, all of them will be updated
$updates = _l10n_update_prepare_updates($updates, NULL, $languages);
$batch = l10n_update_batch_multiple($updates, $mode);
batch_set($batch);
}
else {
drupal_set_message(t('Cannot find any translation updates.'), 'error');
}
}
elseif ($op == t('Refresh information')) {
// Get current version of projects.
l10n_update_build_projects();
// Get available translation updates and update file history.
if ($available = l10n_update_available_releases(TRUE)) {
l10n_update_flag_history($available);
drupal_set_message(t('Fetched information about available updates from the server'));
}
else {
drupal_set_message(t('Failed to fetch information about available updates from the server.'), 'error');
}
}
}
/**
* Page callback: Settings form.
*/
function l10n_update_admin_settings_form($form, &$form_state) {
$form['l10n_update_check_mode'] = array(
'#type' => 'radios',
'#title' => t('Update source'),
'#default_value' => variable_get('l10n_update_check_mode', L10N_UPDATE_CHECK_ALL),
'#options' => _l10n_update_admin_check_options(),
);
$form['l10n_update_import_mode'] = array(
'#type' => 'radios',
'#title' => t('Update mode'),
'#default_value' => variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP),
'#options' => _l10n_update_admin_import_options(),
);
$form['l10n_update_check_frequency'] = array(
'#type' => 'radios',
'#title' => t('Check for updates'),
'#default_value' => variable_get('l10n_update_check_frequency', 0),
'#options' => array(
0 => t('Never (manually)'),
1 => t('Daily'),
7 => t('Weekly'),
),
'#description' => t('Select how frequently you want to automatically check for updated translations for installed modules and themes.'),
);
$form['l10n_update_check_disabled'] = array(
'#type' => 'checkbox',
'#title' => t('Check for updates of disabled modules and themes'),
'#default_value' => variable_get('l10n_update_check_disabled', 0),
'#description' => t('Note that this comes with a performance penalty, so it is not recommended.'),
);
$form['l10n_update_download_store'] = array(
'#title' => t('Store downloaded files'),
'#type' => 'textfield',
'#default_value' => variable_get('l10n_update_download_store', ''),
'#description' => t('A path relative to the Drupal installation directory where translation files will be stored, e.g. sites/all/translations. Saved translation files can be reused by other installations. If left empty the downloaded translation will not be saved.'),
);
return system_settings_form($form);
}
/**
* Additional validation handler for update settings.
*
* Check for existing files directory and creates one when required.
*/
function l10n_update_admin_settings_form_validate($form, &$form_state) {
$form_values = $form_state['values'];
if (!empty($form_values['l10n_update_download_store'])) {
if (!file_prepare_directory($form_values['l10n_update_download_store'], FILE_CREATE_DIRECTORY, 'l10n_update_download_store')) {
form_set_error('l10n_update_download_store', t('The directory %directory does not exist or is not writable.', array('%directory' => $form_values['l10n_update_download_store'])));
watchdog('file system', 'The directory %directory does not exist or is not writable.', array('%directory' => $form_values['l10n_update_download_store']), WATCHDOG_ERROR);
}
}
}
/**
* Get array of import options.
*
* The import options of the Locale module are used but the UI text is altered
* to suit the Localization update cases.
*
* @return
* Keyed array of import options.
*/
function _l10n_update_admin_import_options() {
return array(
LOCALE_IMPORT_OVERWRITE => t('Translation updates replace existing ones, new ones are added'),
LOCALE_UPDATE_OVERRIDE_DEFAULT => t('Edited translations are kept, only previously imported ones are overwritten and new translations are added'),
LOCALE_IMPORT_KEEP => t('All existing translations are kept, only new translations are added.'),
);
}
/**
* Get array of check options.
*
* @return
* Keyed array of source download options.
*/
function _l10n_update_admin_check_options() {
return array(
L10N_UPDATE_CHECK_ALL => t('Local files and remote server.'),
L10N_UPDATE_CHECK_LOCAL => t('Local files only.'),
L10N_UPDATE_CHECK_REMOTE => t('Remote server only.'),
);
}
/**
* Format project update status.
*
* @param $variables
* An associative array containing:
* - projects: An array containing all enabled projects.
* - languages: An array of all enabled languages.
* - history: An array of the current translations per project.
* - available: An array of translation sources per project.
* - updates: An array of available translation updates per project.
* Only recommended translations are listed.
*
* @return string
* HTML output.
*/
function theme_l10n_update_project_status($variables) {
$header = $rows = array();
// Get module and theme data for the project title.
$projects = system_rebuild_module_data();
$projects += system_rebuild_theme_data();
foreach ($variables['projects'] as $name => $project) {
if (isset($variables['history'][$name])) {
if (isset($variables['updates'][$name])) {
$project_status = 'updatable';
$project_class = 'warning';
}
else {
$project_status = 'uptodate';
$project_class = 'ok';
}
}
elseif (isset($variables['available'][$name])) {
$project_status = 'available';
$project_class = 'warning';
}
else {
// Remote information not checked
$project_status = 'unknown';
$project_class = 'unknown';
}
// Get the project title and module version.
$project->title = isset($projects[$name]->info['name']) ? $projects[$name]->info['name'] : '';
$project->module_version = isset($projects[$name]->info['version']) ? $projects[$name]->info['version'] : $project->version;
// Project with related language states.
$row = theme('l10n_update_single_project_wrapper', array(
'project' => $project,
'project_status' => $project_status,
'languages' => $variables['languages'],
'available' => $variables['available'],
'history' => $variables['history'],
'updates' => $variables['updates'],
));
$rows[$project->project_type][] = array(
'data' => array(
array(
'data' => $row,
'class' => 'l10n-update-wrapper collapsed',
),
),
'class' => array($project_class),
);
}
// Build tables of update states grouped by project type. Similar to the
// status report by the Update module.
$output = '';
$project_types = array(
'core' => t('Drupal core'),
'module' => t('Modules'),
'theme' => t('Themes'),
'module-disabled' => t('Disabled modules'),
'theme-disabled' => t('Disabled themes'),
);
foreach ($project_types as $type_name => $type_label) {
if (!empty($rows[$type_name])) {
ksort($rows[$type_name]);
$output .= "\n<h3>" . $type_label . "</h3>\n";
$output .= theme('table', array('header' => $header, 'rows' => $rows[$type_name], 'attributes' => array('class' => array('update l10n-update'))));
}
}
// We use the core update module CSS to re-use the color definitions.
// Plus add our own css and js.
drupal_add_css(drupal_get_path('module', 'update') . '/update.css');
drupal_add_css(drupal_get_path('module', 'l10n_update') . '/css/l10n_update.admin.css');
drupal_add_js(drupal_get_path('module', 'l10n_update') . '/js/l10n_update.js');
return $output;
}
/**
* Format project translation state with states per language.
*
* @param $variables
* An associative array containing:
* - project: Project data object
* - project_status: Project status
* - languages: Available languages.
* @return string
* HTML output.
*/
function theme_l10n_update_single_project_wrapper($variables) {
$project = $variables['project'];
$name = $project->name;
$project_status = $variables['project_status'];
$languages = $variables['languages'];
$history = $variables['history'];
$updates = $variables['updates'];
$availables = $variables['available'];
// Output project title and project summary status.
$output = theme('l10n_update_single_project_status', array(
'project' => $project,
'server' => l10n_update_server($project->l10n_server),
'status' => $project_status,
));
// Translation status per language is displayed in a table, one language per row.
// For each language the current translation is listed. And optionally the
// most recent update.
$rows = array();
foreach ($languages as $lang => $language) {
// Determine current translation status and update status.
$installed = isset($history[$name][$lang]) ? $history[$name][$lang] : NULL;
$update = isset($updates[$name][$lang]) ? $updates[$name][$lang] : NULL;
$available = isset($availables[$name][$lang]) ? $availables[$name][$lang] : NULL;
if ($installed) {
if ($update) {
$status = 'updatable';
$class = 'messages warning';
}
else {
$status = 'uptodate';
$class = 'ok';
}
}
elseif ($available) {
$status = 'available';
$class = 'warning';
}
else {
$status = 'unknown';
$class = 'unknown';
}
// The current translation version.
$row = theme('l10n_update_current_release', array('language' => $language, 'release' => $installed, 'status' => $status));
// If an update is available, add it.
if ($update) {
$row .= theme('l10n_update_available_release', array('release' => $update));
}
$rows[] = array(
'data' => array($row),
'class' => array($class),
);
}
// Output tables with translation status per language.
$output .= '<div class="fieldset-wrapper">' . "\n";
$output .= theme('table', array('header' => array(), 'rows' => $rows));
$output .= "</div>\n";
return $output;
}
/**
* Format a single project translation state.
*
* @param $variables
* An associative array containing:
* - project: project data object.
* - server: (optional) remote server data object.
* - status: project summary status.
* @return string
* HTML output.
*/
function theme_l10n_update_single_project_status($variables) {
$project = $variables['project'];
$server = $variables['server'];
$title = $project->title ? $project->title : $project->name;
$output = '<div class="project">';
$output .= '<span class="project-title">' . check_plain($title) . '</span>' . ' ' . check_plain($project->module_version) ;
if ($server = l10n_update_server($project->l10n_server)) {
$output .= '<span class="project-server">' . t('(translation source: !server)', array('!server' => l($server['name'], $server['link']))) . '</span>';
}
$output .= theme('l10n_update_version_status', array('status' => $variables['status']));
$output .= "</div>\n";
return $output;
}
/**
* Format current translation version.
*
* @param $variables
* An associative array containing:
* - language: Language name.
* - release: Current file data.
* - status: Release status.
* @return string
* HTML output.
*/
function theme_l10n_update_current_release($variables) {
if (isset($variables['release'])) {
$date = $variables['release']->timestamp;
$version = $variables['release']->version;
$text = t('@language: @version (!date)', array('@language' => $variables['language'], '@version' => $version, '!date' => format_date($date, 'custom', 'Y-M-d')));
}
else {
$text = t('@language: <em>No installed translation</em>', array('@language' => $variables['language']));
}
$output = '<div class="language">';
$output .= $text;
$output .= theme('l10n_update_version_status', $variables);
$output .= "</div>\n";
return $output;
}
/**
* Format current translation version.
*
* @param object $release
* Update file data.
* @return string
* HTML output.
*/
function theme_l10n_update_available_release($variables) {
$date = $variables['release']->timestamp;
$version = $variables['release']->version;
if (!empty($variables['release']->fileurl)) {
// Remote file, straight link
$link = l(t('Download'), $variables['release']->fileurl);
}
elseif (!empty($variables['release']->uri)) {
// Local file, try something
$link = l(t('Download'), $variables['release']->uri, array('absolute' => TRUE));
}
$output = '<div class="version version-recommended">';
$output .= t('Recommended version: @version (!date)', array('@version' => $version, '!date' => format_date($date, 'custom', 'Y-M-d')));
$output .= '<span class="version-links">' . $link . '</span>';
$output .= "</div>\n";
return $output;
}
/**
* Format version status with icon.
*
* @param string $status
* Version status: 'uptodate', 'updatable', 'available', 'unknown'.
* @param string $type
* Update type: 'download', 'localfile'.
*
* @return sting
* HTML output.
*/
function theme_l10n_update_version_status($variables) {
$icon = '';
$msg = '';
switch ($variables['status']) {
case 'uptodate':
$icon = theme('image', array('path' => 'misc/watchdog-ok.png', 'alt' => t('ok'), 'title' => t('ok')));
$msg = '<span class="current">' . t('Up to date') . '</span>';
break;
case 'updatable':
$icon = theme('image', array('path' => 'misc/watchdog-warning.png', 'alt' => t('warning'), 'title' => t('warning')));
$msg = '<span class="not-current">' . t('Update available') . '</span>';
break;
case 'available':
$icon = theme('image', array('path' => 'misc/watchdog-warning.png', 'alt' => t('warning'), 'title' => t('warning')));
$msg = '<span class="not-current">' . t('Uninstalled translation available') . '</span>';
break;
case 'unknown':
$icon = theme('image', array('path' => 'misc/watchdog-warning.png', 'alt' => t('warning'), 'title' => t('warning')));
$msg = '<span class="not-supported">' . t('No available translations found') . '</span>';
break;
}
$output = '<div class="version-status">';
$output .= $msg;
$output .= '<span class="icon">' . $icon . '</span>';
$output .= "</div>\n";
return $output;
}
<?php
/**
* @file
* API documentation for Localize updater module.
*/
/**
* Returns available translation servers and server definitions.
*
* @return keyed array of available servers.
* Example: array('localize.drupal.org' => array(
* 'name' => 'localize.drupal.org',
* 'server_url' => 'http://ftp.drupal.org/files/translations/l10n_server.xml',
* 'update_url' => 'http://ftp.drupal.org/files/translations/%core/%project/%project-%release.%language.po',
* ),
* );
*/
function hook_l10n_servers() {
// This hook is used to specify the default localization server(s).
// Additionally server data can be specified on a per project basis in the
// .info file or using the hook_l10n_update_projects_alter().
module_load_include('inc', 'l10n_update');
$server = l10n_update_default_server();
return array($server['name'] => $server );
}
/**
* Alter the list of project to be updated by l10n update.
*
* l10n_update uses the same list of projects as update module. Using this hook
* the list can be altered.
*
* @param array $projects
* Array of projects.
*/
function hook_l10n_update_projects_alter(&$projects) {
// The $projects array contains the project data produced by
// update_get_projects(). A number of the array elements are described in
// the documentation of hook_update_projects_alter().
// In the .info file of a project a localization server can be specified.
// Using this hook the localization server specification can be altered or
// added. The 'l10n path' element is optional but can be specified to override
// the translation download path specified in the 10n_server.xml file.
$projects['existing_example_project'] = array(
'info' => array(
'l10n server' => 'example.com',
'l10n url' => 'http://example.com/files/translations/l10n_server.xml',
'l10n path' => 'http://example.com/files/translations/%core/%project/%project-%release.%language.po',
),
);
// With this hook it is also possible to add a new project wich does not
// exist as a real module or theme project but is treated by the localization
// update module as one. The below data is the minumum to be specified.
// As in the previous example the 'l10n path' element is optional.
$projects['new_example_project'] = array(
'project_type' => 'module',
'name' => 'new_example_project',
'info' => array(
'version' => '6.x-1.5',
'core' => '6.x',
'l10n server' => 'example.com',
'l10n url' => 'http://example.com/files/translations/l10n_server.xml',
'l10n path' => 'http://example.com/files/translations/%core/%project/%project-%release.%language.po',
),
);
}
<?php
/**
* @file
* Reusable API for creating and running l10n update batches.
*/
// module_load_include will not work in batch.
include_once 'l10n_update.check.inc';
/**
* Create a batch to just download files.
*
* @param $updates
* Translations sources to be downloaded.
* Note: All update sources must have a 'fileurl'.
* @return array
* A batch definition for this download.
*/
function l10n_update_batch_download($updates) {
foreach ($updates as $update) {
$id = $update->filename;
$operations[] = array('_l10n_update_batch_download', array($id, $update));
}
return _l10n_update_create_batch($operations);
}
/**
* Create a batch to just import files.
*
* All update sources must have a 'uri'.
*
* @param $updates
* Translations sources to be imported.
* Note: All update sources must have a 'fileurl'.
* @param $import_mode
* Import mode. How to treat existing and modified translations.
* @return array
* A batch definition for this import.
*/
function l10n_update_batch_import($updates, $import_mode) {
foreach ($updates as $update) {
$id = $update->filename;
$operations[] = array('_l10n_update_batch_import', array($id, $update, $import_mode));
}
return _l10n_update_create_batch($operations);
}
/**
* Create a big batch for multiple projects and languages.
*
* @param $updates
* Array of update sources to be run.
* @param $mode
* Import mode. How to treat existing and modified translations.
* @return array
*/
function l10n_update_batch_multiple($updates, $import_mode) {
foreach ($updates as $update) {
$id = $update->filename;
if ($update->type == 'download') {
$operations[] = array('_l10n_update_batch_download', array($id, $update));
$operations[] = array('_l10n_update_batch_import', array($id, NULL, $import_mode));
}
else {
$operations[] = array('_l10n_update_batch_import', array($id, $update, $import_mode));
}
// This one takes always parameters from results.
$operations[] = array('_l10n_update_batch_history', array($id));
}
if (!empty($operations)) {
return _l10n_update_create_batch($operations);
}
}
/**
* Create batch stub for this module.
*
* @param $operations
* Operations to perform in this batch.
* @return array
* A batch definition:
* - 'operations': Batch operations
* - 'title': Batch title.
* - 'init_message': Initial batch UI message.
* - 'error_message': Batch error message.
* - 'file': File containing callback function.
* - 'finished': Batch completed callback function.
*/
function _l10n_update_create_batch($operations = array()) {
$t = get_t();
$batch = array(
'operations' => $operations,
'title' => $t('Updating translation.'),
'init_message' => $t('Downloading and importing files.'),
'error_message' => $t('Error importing interface translations'),
'file' => drupal_get_path('module', 'l10n_update') . '/l10n_update.batch.inc',
'finished' => '_l10n_update_batch_finished',
);
return $batch;
}
/**
* Batch process: Download a file.
*
* @param $id
* Batch id to identify batch results.
* Result of this batch function are stored in $context['result']
* identified by this $id.
* @param $file
* File to be downloaded.
* @param $context
* Batch context array.
*/
function _l10n_update_batch_download($id, $file, &$context) {
$t = get_t();
if (l10n_update_source_download($file)) {
$context['message'] = $t('Importing: %name.', array('%name' => $file->filename));
$context['results'][$id] = array('file' => $file);
}
else {
$context['results'][$id] = array('file' => $file, 'fail' => TRUE);
}
}
/**
* Batch process: Update the download history table.
*
* @param $id
* Batch id to identify batch results.
* Result of this batch function are stored in $context['result']
* identified by this $id.
* @param $context
* Batch context array.
*/
function _l10n_update_batch_history($id, &$context) {
$t = get_t();
// The batch import is performed in a number of steps. History update is
// always the last step. The details of the downloaded/imported file are
// stored in $context['results'] array.
if (isset($context['results'][$id]['file']) && !isset($context['results'][$id]['fail'])) {
$file = $context['results'][$id]['file'];
l10n_update_source_history($file);
$context['message'] = $t('Imported: %name.', array('%name' => $file->filename));
}
}
/**
* Batch process: Import translation file.
*
* This takes a file parameter or continues from previous batch
* which should have downloaded a file.
*
* @param $id
* Batch id to identify batch results.
* Result of this batch function are stored in $context['result']
* identified by this $id.
* @param $file
* File to be imported. If empty, the file will be taken from $context['results'].
* @param $mode
* Import mode. How to treat existing and modified translations.
* @param $context
* Batch context array.
*/
function _l10n_update_batch_import($id, $file, $mode, &$context) {
$t = get_t();
// The batch import is performed in two or three steps.
// If import is performed after file download the file details of the download
// are used which are stored in the $context['results'] array.
// If a locally stored file is imported, the file details are placed in $file.
if (empty($file) && isset($context['results'][$id]['file']) && !isset($context['results'][$id]['fail'])) {
$file = $context['results'][$id]['file'];
}
if ($file) {
if ($import_result = l10n_update_source_import($file, $mode)) {
$context['message'] = $t('Imported: %name.', array('%name' => $file->filename));
$context['results'][$id] = array_merge((array)$context['results'][$id], $import_result, array('file' => $file));
}
else {
$context['results'][$id] = array_merge((array)$context['results'][$id], array('fail' => TRUE), array('file' => $file));
}
}
}
/**
* Batch finished callback: Set result message.
*
* @param $success
* TRUE if batch succesfully completed.
* @param $results
* Batch results.
*/
function _l10n_update_batch_finished($success, $results) {
$totals = array(); // Sum of added, updated and deleted translations.
$total_skip = 0; // Sum of skipped translations
$messages = array(); // User feedback messages.
$project_fail = $project_success = array(); // Project names of succesfull and failed imports.
$t = get_t();
if ($success) {
// Summarize results of added, updated, deleted and skiped translations.
// Added, updated and deleted are summarized per language to be displayed accordingly.
foreach ($results as $result) {
if (isset($result['fail'])) {
// Collect project names of the failed imports.
$project_fail[$result['file']->name] = $result['file']->name;
}
else {
$language = $result['language'];
// Initialize variables to prevent PHP Notices.
if (!isset($totals[$language])) {
$totals[$language] = array();
$totals[$language]['add'] = $totals[$language]['update'] = $totals[$language]['delete'] = 0;
}
// Summarize added, updated, deleted and skiped translations.
$totals[$language]['add'] += $result['add'];
$totals[$language]['update'] += $result['update'];
$totals[$language]['delete'] += $result['delete'];
$total_skip += $result['skip'];
// Collect project names of the succesfull imports.
$project_success[$result['file']->name] = $result['file']->name;
}
}
// Messages of succesfull translation update results.
if ($project_success) {
$messages[] = format_plural(count($project_success), 'One project updated: @projects.', '@count projects updated: @projects.', array('@projects' => implode(', ', $project_success)));
$languages = language_list();
foreach ($totals as $language => $total) {
$messages[] = $t('%language translation strings added: !add, updated: !update, deleted: !delete.', array(
'%language' => $languages[$language]->name,
'!add' => $total['add'],
'!update' => $total['update'],
'!delete' => $total['delete'],
));
}
drupal_set_message(implode("<br />\n", $messages));
// Warning for disallowed HTML.
if ($total_skip) {
drupal_set_message(
format_plural(
$total_skip,
'One translation string was skipped because it contains disallowed HTML. See !log_messages for details.',
'@count translation strings were skipped because they contain disallowed HTML. See !log_messages for details.',
array('!log_messages' => l(t('Recent log messages'), 'admin/reports/dblog'))),
'warning');
}
}
// Error for failed imports.
if ($project_fail) {
drupal_set_message(
format_plural(
count($project_fail),
'Translations of one project were not imported: @projects.',
'Translations of @count projects were not imported: @projects',
array('@projects' => implode(', ', $project_fail))),
'error');
}
}
else {
drupal_set_message($t('Error importing translations.'), 'error');
}
}
<?php
/**
* @file
* Reusable API for l10n remote updates using $source objects
*
* These functions may not be safe for the installer as they use variables and report using watchdog
*/
/**
* Threshold for timestamp comparison.
*
* Eliminates a difference between the download time
* (Database: l10n_update_file.timestamp) and the actual .po file timestamp.
*/
define('L10N_UPDATE_TIMESTAMP_THRESHOLD', 2);
module_load_include('inc', 'l10n_update');
/**
* Fetch update information for all projects / all languages.
*
* @param boolean $refresh
* TRUE = refresh the release data.
* We refresh anyway if the data is older than a day.
*
* @return array
* Available releases indexed by project and language.
*/
function l10n_update_available_releases($refresh = FALSE) {
$frequency = variable_get('l10n_update_check_frequency', 0) * 24 * 3600;
if (!$refresh && ($cache = cache_get('l10n_update_available_releases', 'cache_l10n_update')) && (!$frequency || $cache->created > REQUEST_TIME - $frequency)) {
return $cache->data;
}
else {
$projects = l10n_update_get_projects(TRUE);
$languages = l10n_update_language_list();
$local = variable_get('l10n_update_check_mode', L10N_UPDATE_CHECK_ALL) & L10N_UPDATE_CHECK_LOCAL;
$remote = variable_get('l10n_update_check_mode', L10N_UPDATE_CHECK_ALL) & L10N_UPDATE_CHECK_REMOTE;
$available = l10n_update_check_projects($projects, array_keys($languages), $local, $remote);
cache_set('l10n_update_available_releases', $available, 'cache_l10n_update', $frequency ? REQUEST_TIME + $frequency : CACHE_PERMANENT);
return $available;
}
}
/**
* Check latest release for project, language.
*
* @param $projects
* Projects to check (objects).
* @param $languages
* Array of language codes to check, none to check all.
* @param $check_local
* Check local translation file.
* @param $check_remote
* Check remote translation file.
*
* @return array
* Available sources indexed by project, language.
*/
function l10n_update_check_projects($projects, $languages = NULL, $check_local = TRUE, $check_remote = TRUE) {
$languages = $languages ? $languages : array_keys(l10n_update_language_list());
$result = array();
foreach ($projects as $name => $project) {
foreach ($languages as $lang) {
$source = l10n_update_source_build($project, $lang);
if ($update = l10n_update_source_check($source, $check_local, $check_remote)) {
$result[$name][$lang] = $update;
}
}
}
return $result;
}
/**
* Compare available releases with history and get list of downloadable updates.
*
* @param $history
* Update history of projects.
* @param $available
* Available project releases.
* @return array
* Projects to be updated: 'not yet downloaded', 'newer timestamp available',
* 'new version available'.
* Up to date projects are not included in the array.
*/
function l10n_update_build_updates($history, $available) {
$updates = array();
foreach ($available as $name => $project_updates) {
foreach ($project_updates as $lang => $update) {
if (!empty($update->timestamp)) {
$current = !empty($history[$name][$lang]) ? $history[$name][$lang] : NULL;
// Add when not current, timestamp newer or version difers (newer version)
if (_l10n_update_source_compare($current, $update) == -1 || $current->version != $update->version) {
$updates[$name][$lang] = $update;
}
}
}
}
return $updates;
}
/**
* Check updates for active projects and languages.
*
* @param $count
* Number of package translations to check.
* @param $before
* Unix timestamp, check only updates that haven't been checked for this time.
* @param $limit
* Maximum number of updates to do. We check $count translations
* but we stop after we do $limit updates.
* @return array
*/
function l10n_update_check_translations($count, $before, $limit = 1) {
$projects = l10n_update_get_projects();
$updated = $checked = array();
// Select active projects x languages ordered by last checked time
$q = db_select('l10n_update_project', 'p');
$q->leftJoin('l10n_update_file', 'f', 'p.name = f.project');
$q->innerJoin('languages', 'l', 'l.language = f.language');
$q->condition('p.status', 1);
$q->condition('l.enabled', 1);
// If the file is not there, or it is there, but we did not check since $before.
$q->condition(db_or()->isNull('f.status')->condition(db_and()->condition('f.status', 1)->condition('f.last_checked', $before, '<')));
$q->range(0, $count);
$q->fields('p', array('name'));
$q->fields('f');
$q->addField('l', 'language', 'lang');
$q->orderBy('last_checked');
$result = $q->execute();
if ($result) {
$local = variable_get('l10n_update_check_mode', L10N_UPDATE_CHECK_ALL) & L10N_UPDATE_CHECK_LOCAL;
$remote = variable_get('l10n_update_check_mode', L10N_UPDATE_CHECK_ALL) & L10N_UPDATE_CHECK_REMOTE;
foreach ($result as $check) {
if (count($updated) >= $limit) {
break;
}
$checked[] = $check;
if (!empty($projects[$check->name])) {
$project = $projects[$check->name];
$update = NULL;
$source = l10n_update_source_build($project, $check->lang);
$current = $check->filename ? $check : NULL;
if ($available = l10n_update_source_check($source, $local, $remote)) {
if (!$current || _l10n_update_source_compare($current, $available) == -1 || $current->version != $available->version) {
$update = $available;
}
}
if ($update) {
// The update functions will update data and timestamps too
l10n_update_source_update($update, variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP));
$updated[] = $update;
}
elseif ($current) {
// No update available, just update timestamp for this row
db_update('l10n_update_file')
->fields(array(
'last_checked' => REQUEST_TIME,
))
->condition('project', $current->project)
->condition('language', $current->language)
->execute();
}
elseif ($source) {
// Create a new record just for keeping last checked time
$source->last_checked = REQUEST_TIME;
drupal_write_record('l10n_update_file', $source);
}
}
}
}
return array($checked, $updated);
}
/**
* Build abstract translation source, to be mapped to a file or a download.
*
* @param $project
* Project object.
* @param $langcode
* Language code.
* @param $filename
* File name of translation file. May contains placeholders.
* @return object
* Source object, which may have these properties:
* - 'project': Project name.
* - 'language': Language code.
* - 'type': Source type 'download' or 'localfile'.
* - 'uri': Local file path.
* - 'fileurl': Remote file URL for downloads.
* - 'filename': File name.
* - 'keep': TRUE to keep the downloaded file.
* - 'timestamp': Last update time of the file.
*/
function l10n_update_source_build($project, $langcode, $filename = L10N_UPDATE_DEFAULT_FILENAME) {
$source = clone $project;
$source->project = $project->name;
$source->language = $langcode;
$source->filename = l10n_update_build_string($source, $filename);
return $source;
}
/**
* Check local and remote sources for the file.
*
* @param $source
* Translation source object.
* @see l10n_update_source_build()
* @param $check_local
* File object of local translation file.
* @param $check_remote
* File object of remote translation file.
* @return object
* File object of most recent translation; local or remote.
*/
function l10n_update_source_check($source, $check_local = TRUE, $check_remote = TRUE) {
$local = $remote = NULL;
if ($check_local) {
$check = clone $source;
if (l10n_update_source_check_file($check)) {
$local = $check;
}
}
if ($check_remote) {
$check = clone $source;
if (l10n_update_source_check_download($check)) {
$remote = $check;
}
}
// Get remote if newer than local only, they both can be empty
return _l10n_update_source_compare($local, $remote) < 0 ? $remote : $local;
}
/**
* Check remote file object.
*
* @param $source
* Remote translation file object. The object will be update
* with data of the remote file:
* - 'type': Fixed value 'download'.
* - 'fileurl': File name and path.
* - 'timestamp': Last updated time.
* @see l10n_update_source_build()
* @return object
* An object containing the HTTP request headers, response code, headers,
* data, redirect status and updated timestamp.
* NULL if failure.
*/
function l10n_update_source_check_download($source) {
$url = l10n_update_build_string($source, $source->l10n_path);
$result = l10n_update_http_check($url);
if ($result && !empty($result->updated)) {
$source->type = 'download';
// There may have been redirects so we store the resulting url
$source->fileurl = isset($result->redirect_url) ? $result->redirect_url : $url;
$source->timestamp = $result->updated;
return $result;
}
}
/**
* Check whether we've got the file in the filesystem under 'translations'.
*
* It will search, similar to modules and themes:
* - translations
* - sites/all/translations
* - sites/mysite/translations
*
* Using name as the key will return just the last one found.
*
* @param $source
* Translation file object. The object will be updated with data of local file.
* - 'type': Fixed value 'localfile'.
* - 'uri': File name and path.
* - 'timestamp': Last updated time.
* @see l10n_update_source_build()
* @param $directory
* Files directory.
* @return Object
* File object (filename, basename, name)
* NULL if failure.
*/
function l10n_update_source_check_file($source, $directory = 'translations') {
$filename = '/' . preg_quote($source->filename) . '$/';
// Using the 'name' key will return
if ($files = drupal_system_listing($filename, $directory, 'name', 0)) {
$file = current($files);
$source->type = 'localfile';
$source->uri = $file->uri;
$source->timestamp = filemtime($file->uri);
return $file;
}
}
/**
* Download and import or just import source, depending on type.
*
* @param $source
* Translation source object with information about the file location.
* Object will be updated with :
* - 'last_checked': Timestamp of current time;
* - 'import_date': Timestamp of current time;
* @param $mode
* Download mode. How to treat exising and modified translations.
* @return boolean
* TRUE on success, NULL on failure.
*/
function l10n_update_source_update($source, $mode) {
if ($source->type == 'localfile' || l10n_update_source_download($source)) {
if (l10n_update_source_import($source, $mode)) {
l10n_update_source_history($source);
return TRUE;
}
}
}
/**
* Import source into locales table.
*
* @param $source
* Translation source object with information about the file location.
* Object will be updated with :
* - 'last_checked': Timestamp of current time;
* - 'import_date': Timestamp of current time;
* @param $mode
* Download mode. How to treat exising and modified translations.
* @return boolean
* Result array on success, FALSE on failure.
*/
function l10n_update_source_import($source, $mode) {
if (!empty($source->uri) && $result = l10n_update_import_file($source->uri, $source->language, $mode)) {
$source->last_checked = REQUEST_TIME;
// We override the file timestamp here. The default file time stamp is the
// release date from the l.d.o server. We change the timestamp to the
// creation time on the webserver. On multi sites that share a common
// sites/all/translations directory, the sharing sites use the local file
// creation date as release date. Without this correction the local
// file is always newer than the l.d.o. file, which results in unnecessary
// translation import.
$source->timestamp = time();
return $result;
}
}
/**
* Download source file from remote server.
*
* If succesful this function returns the downloaded file in two ways:
* - As a temporary $file object
* - As a file path on the $source->uri property.
*
* @param $source
* Source object with all parameters
* - 'fileurl': url to download.
* - 'uri': alternate destination. If not present a temporary file
* will be used and the path returned here.
* @return object
* $file object if download successful.
*/
function l10n_update_source_download($source) {
if (!empty($source->uri)) {
$destination = $source->uri;
}
elseif ($directory = variable_get('l10n_update_download_store', '')) {
$destination = $directory . '/' . $source->filename;
}
else {
$destination = NULL;
}
if ($file = l10n_update_download_file($source->fileurl, $destination)) {
$source->uri = $file;
return $file;
}
}
/**
* Update the file history table and delete the file if temporary.
*
* @param $file
* Source object representing the file just imported or downloaded.
*/
function l10n_update_source_history($file) {
// Update history table
l10n_update_file_history($file);
// If it's a downloaded file and not marked for keeping, delete the file.
if ($file->type == 'download' && empty($file->keep)) {
file_unmanaged_delete($file->uri);
$file->uri = '';
}
}
/**
* Compare two update sources, looking for the newer one (bigger timestamp).
*
* This function can be used as a callback to compare two source objects.
*
* @param $current
* Source object of current project.
* @param $update
* Source object of available update.
* @return integer
* - '-1': $current < $update OR $current is missing
* - '0': $current == $update OR both $current and $updated are missing
* - '1': $current > $update OR $update is missing
*/
function _l10n_update_source_compare($current, $update) {
if ($current && $update) {
if (abs($current->timestamp - $update->timestamp) < L10N_UPDATE_TIMESTAMP_THRESHOLD) {
return 0;
}
else {
return $current->timestamp > $update->timestamp ? 1 : -1;
}
}
elseif ($current && !$update) {
return 1;
}
elseif (!$current && $update) {
return -1;
}
else {
return 0;
}
}
/**
* Prepare update list.
*
* @param $updates
* Array of update sources that may be indexed in multiple ways.
* @param $projects
* Array of project names to be included, others will be filtered out.
* @param $languages
* Array of language codes to be included, others will be filtered out.
* @return array
* Plain array of filtered updates with directory applied.
*/
function _l10n_update_prepare_updates($updates, $projects = NULL, $languages = NULL) {
$result = array();
foreach ($updates as $key => $update) {
if (is_array($update)) {
// It is a sub array of updates yet, process and merge
$result = array_merge($result, _l10n_update_prepare_updates($update, $projects, $languages));
}
elseif ((!$projects || in_array($update->project, $projects)) && (!$languages || in_array($update->language, $languages))) {
$directory = variable_get('l10n_update_download_store', '');
if ($directory && empty($update->uri)) {
// If we have a destination folder set just if we have no uri
if (empty($update->uri)) {
$update->uri = $directory . '/' . $update->filename;
$update->keep = TRUE;
}
}
$result[] = $update;
}
}
return $result;
}
/**
* Language refresh. Runs a batch for loading the selected languages.
*
* To be used after adding a new language.
*
* @param $languages
* Array of language codes to check and download.
*/
function l10n_update_language_refresh($languages) {
$projects = l10n_update_get_projects();
if ($available = l10n_update_check_projects($projects, $languages)) {
$history = l10n_update_get_history();
if ($updates = l10n_update_build_updates($history, $available)) {
module_load_include('batch.inc', 'l10n_update');
// Filter out updates in other languages. If no languages, all of them will be updated
$updates = _l10n_update_prepare_updates($updates);
$batch = l10n_update_batch_multiple($updates, variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP));
batch_set($batch);
}
}
}
<?php
/**
* @file
* Drush interface to l10n-update functionalities.
*/
/**
* Implements hook_drush_command().
*/
function l10n_update_drush_command() {
$commands = array();
$commands['l10n-update-refresh'] = array(
'description' => 'Refresh available information.',
);
$commands['l10n-update-status'] = array(
'description' => 'Show translation status of available projects.',
'options' => array(
'languages' => 'Comma separated list of languages. Defaults to all available languages.',
)
);
$commands['l10n-update'] = array(
'description' => 'Update translations.',
'options' => array(
'languages' => 'Comma separated list of languages. Defaults to all available languages.',
'mode' => 'Allowed values: overwrite, keep. Default value: keep.'
),
);
return $commands;
}
/**
* Callback for command l10n-update-refresh.
*/
function drush_l10n_update_refresh() {
module_load_include('admin.inc', 'l10n_update');
// Fake $form_state to leverage _submit function.
$form_state = array(
'values' => array('op' => t('Refresh information'))
);
l10n_update_admin_import_form_submit(NULL, $form_state);
}
/**
* Validate command l10n-update-status.
*/
function drush_l10n_update_status_validate() {
_drush_l10n_update_validate_languages();
}
/**
* Callback for command l10n-update-status.
*/
function drush_l10n_update_status() {
$updates = _drush_l10n_update_get_updates();
if (!is_null($updates)) {
$languages = drush_get_option('languages');
// Table header.
$table = array();
$header = array(dt('Project'));
foreach ($languages as $lang => $language) {
$header[] = $language . ' status';
}
$table[] = $header;
// Iterate projects to obtain per language status.
$projects = l10n_update_get_projects();
$history = l10n_update_get_history();
foreach ($projects as $name => $project) {
$row = array();
// Project.
$title = isset($project->title) ? $project->title : $project->name;
$row[] = $title . ' ' . $project->version;
// Language status.
foreach ($languages as $lang => $language) {
$current = isset($history[$name][$lang]) ? $history[$name][$lang] : NULL;
$update = isset($updates[$name][$lang]) ? $updates[$name][$lang] : NULL;
if ($update) {
$row[] = ($update->type == 'download') ? t('Remote update available'):t('Local update available');
}
elseif ($current) {
$row[] = t('Up to date');
}
else {
$row[] = t('No information');
}
}
$table[] = $row;
}
drush_print_table($table, TRUE);
}
else {
drush_log(dt('No projects or languages to update.'), 'ok');
}
}
/**
* Validate command l10n-update.
*/
function drush_l10n_update_validate() {
_drush_l10n_update_validate_languages();
// Check provided update mode is valid.
$mode = drush_get_option('mode', 'keep');
if (!in_array($mode, array('keep', 'overwrite'))) {
return drush_set_error('L10N_UPDATE_INVALID_MODE', dt('Invalid update mode. Valid options are keep, overwrite.'));
}
}
/**
* Callback for command l10n-update.
*/
function drush_l10n_update() {
$updates = _drush_l10n_update_get_updates();
if (!is_null($updates)) {
if (count($updates) > 0) {
drush_log(dt('Found @count projects to update.', array('@count' => count($updates))), 'status');
// Batch update all projects for selected languages.
$mode = drush_get_option('mode', 'keep');
$languages = drush_get_option('languages');
module_load_include('batch.inc', 'l10n_update');
$updates = _l10n_update_prepare_updates($updates, NULL, array_keys($languages));
$batch = l10n_update_batch_multiple($updates, $mode);
drush_log($batch['title'], 'status');
drush_log($batch['init_message'], 'status');
batch_set($batch);
drush_backend_batch_process();
}
else {
drush_log(dt('All translations up to date'), 'status');
}
}
}
/**
* Helper function to validate languages.
*
* Used by _validate hooks.
* 1. Check other languages than english are available.
* 2. Check user provided languages are valid.
*/
function _drush_l10n_update_validate_languages() {
// Check there're installed other languages than english.
$installed_languages = l10n_update_language_list();
if (empty($installed_languages)) {
return drush_set_error('L10N_UPDATE_NO_LANGUAGES', dt('No languages to update.'));
}
// Check provided languages are valid.
$languages = drush_get_option('languages', '');
$languages = array_map('trim', _convert_csv_to_array($languages));
if (count($languages)) {
foreach ($languages as $key => $lang) {
if (!isset($installed_languages[$lang])) {
drush_set_error('L10N_UPDATE_INVALID_LANGUAGE', dt('Language @lang is not installed.', array('@lang' => $lang)));
}
else {
unset($languages[$key]);
$languages[$lang] = $installed_languages[$lang];
}
}
if (drush_get_error() != DRUSH_SUCCESS) {
drush_print(dt('Available languages: @languages', array('@languages' => implode(', ', array_keys($installed_languages)))));
}
}
else {
$languages = $installed_languages;
}
drush_set_option('languages', $languages);
}
/**
* Helper function to obtain $updates.
*
* @return $updates array or NULL.
*/
function _drush_l10n_update_get_updates() {
$projects = l10n_update_get_projects();
if ($projects) {
$history = l10n_update_get_history();
drush_log(dt('Fetching update information for all projects / all languages.'), 'status');
module_load_include('check.inc', 'l10n_update');
$available = l10n_update_available_releases();
$updates = l10n_update_build_updates($history, $available);
return $updates;
}
else {
drush_log(dt('No projects or languages to update.'), 'ok');
}
}
<?php
/**
* @file
* Reusable API for l10n remote updates.
*/
include_once DRUPAL_ROOT . '/includes/locale.inc';
module_load_include('locale.inc', 'l10n_update');
/**
* Default update server, filename and URL.
*/
define('L10N_UPDATE_DEFAULT_SERVER', 'localize.drupal.org');
define('L10N_UPDATE_DEFAULT_SERVER_URL', 'http://localize.drupal.org/l10n_server.xml');
define('L10N_UPDATE_DEFAULT_UPDATE_URL', 'http://ftp.drupal.org/files/translations/%core/%project/%project-%release.%language.po');
// Translation filename, will be used just for local imports
define('L10N_UPDATE_DEFAULT_FILENAME', '%project-%release.%language.po');
// Translation status: String imported from po
define('L10N_UPDATE_STRING_DEFAULT', 0);
// Translation status: Custom string, overridden original import
define('L10N_UPDATE_STRING_CUSTOM', 1);
/**
* Retrieve data for default server.
*
* @return array
* Server parameters:
* name : Localization server name
* server_url : Localization server URL where language list can be retrieved.
* update_url : URL containing po file pattern.
*/
function l10n_update_default_server() {
return array(
'name' => variable_get('l10n_update_default_server', L10N_UPDATE_DEFAULT_SERVER),
'server_url' => variable_get('l10n_update_default_server_url', L10N_UPDATE_DEFAULT_SERVER_URL),
'update_url' => variable_get('l10n_update_default_update_url', L10N_UPDATE_DEFAULT_UPDATE_URL),
);
}
/**
* Download and import remote translation file.
*
* @param $download_url
* Download URL.
* @param $locale
* Language code.
* @param $mode
* Download mode. How to treat exising and modified translations.
*
* @return boolean
* TRUE on success.
*/
function l10n_update_download_import($download_url, $locale, $mode = LOCALE_IMPORT_OVERWRITE) {
if ($file = l10n_update_download_file($download_url)) {
$result = l10n_update_import_file($file, $locale, $mode);
return $result;
}
}
/**
* Import local file into the database.
*
* @param $file
* File object of localy stored file
* or path to localy stored file.
* @param $locale
* Language code.
* @param $mode
* Download mode. How to treat exising and modified translations.
*
* @return boolean
* Result array on success. FALSE on failure
*/
function l10n_update_import_file($file, $locale, $mode = LOCALE_IMPORT_OVERWRITE) {
// If the file is a uri, create a $file object
if (is_string($file)) {
$uri = $file;
$file = new stdClass();
$file->uri = $uri;
$file->filename = $uri;
}
return _l10n_update_locale_import_po($file, $locale, $mode, 'default');
}
/**
* Get remote file and download it to a temporary path.
*
* @param $download_url
* URL of remote file.
* @param $destination
* URL of local destination file. By default the download will be stored
* in a temporary file.
*/
function l10n_update_download_file($download_url, $destination = NULL) {
$t = get_t();
$variables['%download_link'] = $download_url;
// Create temporary file or use specified file destination.
// Temporary files get a 'translation-' file name prefix.
$file = $destination ? $destination : drupal_tempnam(file_directory_temp(), 'translation-');
if ($file) {
$variables['%tmpfile'] = $file;
// We download and store the file (in one if statement! Isnt't that neat ;) ).
// @todo remove the timeout once we use the batch API to download the files.
if (($contents = drupal_http_request($download_url, array('timeout' => 90))) && $contents->code == 200 && $file_result = file_put_contents($file, $contents->data)) {
watchdog('l10n_update', 'Successfully downloaded %download_link to %tmpfile', $variables);
return $file;
}
else {
if (isset($contents->error)) {
watchdog('l10n_update', 'An error occured during the download operation: %error.', array('%error' => $contents->error), WATCHDOG_ERROR);
}
elseif (isset($contents->code) && $contents->code != 200) {
watchdog('l10n_update', 'An error occured during the download operation: HTTP status code %code.', array('%code' => $contents->code), WATCHDOG_ERROR);
}
if (isset($file_result)) {
// file_put_contents() was called but returned FALSE.
watchdog('l10n_update', 'Unable to save %download_link file to %tmpfile.', $variables, WATCHDOG_ERROR);
}
}
}
else {
$variables['%tmpdir'] = file_directory_temp();
watchdog('l10n_update', 'Error creating temporary file for download in %tmpdir. Remote file is %download_link.', $variables, WATCHDOG_ERROR);
}
}
/**
* Get names for the language list from locale system.
*
* @param $string_list
* Comma separated list of language codes.
* Language codes must exist in languages from _locale_get_predefined_list().
* @return array
* Array of language names keyed by language code.
*/
function l10n_update_get_language_names($string_list) {
$t = get_t();
$language_codes = array_map('trim', explode(',', $string_list));
$languages = _locale_get_predefined_list();
$result = array();
foreach ($language_codes as $lang) {
if (array_key_exists($lang, $languages)) {
// Try to use verbose locale name
$name = $lang;
$name = $languages[$name][0] . (isset($languages[$name][1]) ? ' ' . $t('(@language)', array('@language' => $languages[$name][1])) : '');
$result[$lang] = $name;
}
}
return $result;
}
/**
* Build project data as an object.
*
* @param $name
* Project name.
* @param $version
* Project version.
* @param $server
* Localisation server name.
* @param $path
* Localisation server URL.
* @return object
* Project object containing the supplied data.
*/
function _l10n_update_build_project($name, $version = NULL, $server = L10N_UPDATE_DEFAULT_SERVER, $path = L10N_UPDATE_DEFAULT_SERVER_URL) {
$project = new stdClass();
$project->name = $name;
$project->version = $version;
$project->l10n_server = $server;
$project->l10n_path = $path;
return $project;
}
/**
* Update the file history table.
*
* @param $file
* Object representing the file just imported or downloaded.
* @return integer
* FALSE on failure. Otherwise SAVED_NEW or SAVED_UPDATED.
* @see drupal_write_record()
*/
function l10n_update_file_history($file) {
// Update or write new record
if (db_query("SELECT project FROM {l10n_update_file} WHERE project = :project AND language = :language", array(':project' => $file->project, ':language' => $file->language))->fetchField()) {
$update = array('project', 'language');
}
else {
$update = array();
}
return drupal_write_record('l10n_update_file', $file, $update);
}
/**
* Delete the history of downloaded translations.
*
* @param string $langcode
* Language code of the file history to be deleted.
*/
function l10n_update_delete_file_history($langcode) {
db_delete('l10n_update_file')
->condition('language', $langcode)
->execute();
}
/**
* Flag the file history as up to date.
*
* Compare history data in the {l10n_update_file} table with translations
* available at translations server(s). Update the 'last_checked' timestamp of
* the files which are up to date.
*
* @param $available
* Available translations as retreived from remote server.
*/
function l10n_update_flag_history($available) {
if ($history = l10n_update_get_history()) {
foreach($history as $name => $project) {
foreach ($project as $langcode => $current) {
if (isset($available[$name][$langcode])) {
$update = $available[$name][$langcode];
// When the available update is equal to the current translation the current
// is marked checked in the {l10n_update_file} table.
if (_l10n_update_source_compare($current, $update) == 0 && $current->version == $update->version) {
db_update('l10n_update_file')
->fields(array(
'last_checked' => REQUEST_TIME,
))
->condition('project', $current->project)
->condition('language', $current->language)
->execute();
}
}
}
}
}
}
/**
* Check if remote file exists and when it was last updated.
*
* @param $url
* URL of remote file.
* @param $headers
* HTTP request headers.
* @return object
* Result object containing the HTTP request headers, response code, headers,
* data, redirect status and updated timestamp.
* @see l10n_update_http_request()
*/
function l10n_update_http_check($url, $headers = array()) {
$result = l10n_update_http_request($url, array('headers' => $headers, 'method' => 'HEAD'));
if ($result && $result->code == '200') {
$result->updated = isset($result->headers['last-modified']) ? strtotime($result->headers['last-modified']) : 0;
}
return $result;
}
/**
* Perform an HTTP request.
*
* We cannot use drupal_http_request() at install, see http://drupal.org/node/527484
*
* This is a flexible and powerful HTTP client implementation. Correctly
* handles GET, POST, PUT or any other HTTP requests. Handles redirects.
*
* @param $url
* A string containing a fully qualified URI.
* @param array $options
* (optional) An array that can have one or more of the following elements:
* - headers: An array containing request headers to send as name/value pairs.
* - method: A string containing the request method. Defaults to 'GET'.
* - data: A string containing the request body, formatted as
* 'param=value&param=value&...'. Defaults to NULL.
* - max_redirects: An integer representing how many times a redirect
* may be followed. Defaults to 3.
* - timeout: A float representing the maximum number of seconds the function
* call may take. The default is 30 seconds. If a timeout occurs, the error
* code is set to the HTTP_REQUEST_TIMEOUT constant.
* - context: A context resource created with stream_context_create().
*
* @return object
* An object that can have one or more of the following components:
* - request: A string containing the request body that was sent.
* - code: An integer containing the response status code, or the error code
* if an error occurred.
* - protocol: The response protocol (e.g. HTTP/1.1 or HTTP/1.0).
* - status_message: The status message from the response, if a response was
* received.
* - redirect_code: If redirected, an integer containing the initial response
* status code.
* - redirect_url: If redirected, a string containing the redirection location.
* - error: If an error occurred, the error message. Otherwise not set.
* - headers: An array containing the response headers as name/value pairs.
* HTTP header names are case-insensitive (RFC 2616, section 4.2), so for
* easy access the array keys are returned in lower case.
* - data: A string containing the response body that was received.
*/
function l10n_update_http_request($url, array $options = array()) {
$result = new stdClass();
// Parse the URL and make sure we can handle the schema.
$uri = @parse_url($url);
if ($uri == FALSE) {
$result->error = 'unable to parse URL';
$result->code = -1001;
return $result;
}
if (!isset($uri['scheme'])) {
$result->error = 'missing schema';
$result->code = -1002;
return $result;
}
timer_start(__FUNCTION__);
// Merge the default options.
$options += array(
'headers' => array(),
'method' => 'GET',
'data' => NULL,
'max_redirects' => 3,
'timeout' => 30.0,
'context' => NULL,
);
// stream_socket_client() requires timeout to be a float.
$options['timeout'] = (float) $options['timeout'];
switch ($uri['scheme']) {
case 'http':
case 'feed':
$port = isset($uri['port']) ? $uri['port'] : 80;
$socket = 'tcp://' . $uri['host'] . ':' . $port;
// RFC 2616: "non-standard ports MUST, default ports MAY be included".
// We don't add the standard port to prevent from breaking rewrite rules
// checking the host that do not take into account the port number.
$options['headers']['Host'] = $uri['host'] . ($port != 80 ? ':' . $port : '');
break;
case 'https':
// Note: Only works when PHP is compiled with OpenSSL support.
$port = isset($uri['port']) ? $uri['port'] : 443;
$socket = 'ssl://' . $uri['host'] . ':' . $port;
$options['headers']['Host'] = $uri['host'] . ($port != 443 ? ':' . $port : '');
break;
default:
$result->error = 'invalid schema ' . $uri['scheme'];
$result->code = -1003;
return $result;
}
if (empty($options['context'])) {
$fp = @stream_socket_client($socket, $errno, $errstr, $options['timeout']);
}
else {
// Create a stream with context. Allows verification of a SSL certificate.
$fp = @stream_socket_client($socket, $errno, $errstr, $options['timeout'], STREAM_CLIENT_CONNECT, $options['context']);
}
// Make sure the socket opened properly.
if (!$fp) {
// When a network error occurs, we use a negative number so it does not
// clash with the HTTP status codes.
$result->code = -$errno;
$result->error = trim($errstr) ? trim($errstr) : t('Error opening socket @socket', array('@socket' => $socket));
// Mark that this request failed. This will trigger a check of the web
// server's ability to make outgoing HTTP requests the next time that
// requirements checking is performed.
// See system_requirements()
// variable_set('drupal_http_request_fails', TRUE);
return $result;
}
// Construct the path to act on.
$path = isset($uri['path']) ? $uri['path'] : '/';
if (isset($uri['query'])) {
$path .= '?' . $uri['query'];
}
// Merge the default headers.
$options['headers'] += array(
'User-Agent' => 'Drupal (+http://drupal.org/)',
);
// Only add Content-Length if we actually have any content or if it is a POST
// or PUT request. Some non-standard servers get confused by Content-Length in
// at least HEAD/GET requests, and Squid always requires Content-Length in
// POST/PUT requests.
$content_length = strlen($options['data']);
if ($content_length > 0 || $options['method'] == 'POST' || $options['method'] == 'PUT') {
$options['headers']['Content-Length'] = $content_length;
}
// If the server URL has a user then attempt to use basic authentication.
if (isset($uri['user'])) {
$options['headers']['Authorization'] = 'Basic ' . base64_encode($uri['user'] . (!empty($uri['pass']) ? ":" . $uri['pass'] : ''));
}
// If the database prefix is being used by SimpleTest to run the tests in a copied
// database then set the user-agent header to the database prefix so that any
// calls to other Drupal pages will run the SimpleTest prefixed database. The
// user-agent is used to ensure that multiple testing sessions running at the
// same time won't interfere with each other as they would if the database
// prefix were stored statically in a file or database variable.
$test_info = &$GLOBALS['drupal_test_info'];
if (!empty($test_info['test_run_id'])) {
$options['headers']['User-Agent'] = drupal_generate_test_ua($test_info['test_run_id']);
}
$request = $options['method'] . ' ' . $path . " HTTP/1.0\r\n";
foreach ($options['headers'] as $name => $value) {
$request .= $name . ': ' . trim($value) . "\r\n";
}
$request .= "\r\n" . $options['data'];
$result->request = $request;
// Calculate how much time is left of the original timeout value.
$timeout = $options['timeout'] - timer_read(__FUNCTION__) / 1000;
if ($timeout > 0) {
stream_set_timeout($fp, floor($timeout), floor(1000000 * fmod($timeout, 1)));
fwrite($fp, $request);
}
// Fetch response. Due to PHP bugs like http://bugs.php.net/bug.php?id=43782
// and http://bugs.php.net/bug.php?id=46049 we can't rely on feof(), but
// instead must invoke stream_get_meta_data() each iteration.
$info = stream_get_meta_data($fp);
$alive = !$info['eof'] && !$info['timed_out'];
$response = '';
while ($alive) {
// Calculate how much time is left of the original timeout value.
$timeout = $options['timeout'] - timer_read(__FUNCTION__) / 1000;
if ($timeout <= 0) {
$info['timed_out'] = TRUE;
break;
}
stream_set_timeout($fp, floor($timeout), floor(1000000 * fmod($timeout, 1)));
$chunk = fread($fp, 1024);
$response .= $chunk;
$info = stream_get_meta_data($fp);
$alive = !$info['eof'] && !$info['timed_out'] && $chunk;
}
fclose($fp);
if ($info['timed_out']) {
$result->code = HTTP_REQUEST_TIMEOUT;
$result->error = 'request timed out';
return $result;
}
// Parse response headers from the response body.
list($response, $result->data) = explode("\r\n\r\n", $response, 2);
$response = preg_split("/\r\n|\n|\r/", $response);
// Parse the response status line.
list($protocol, $code, $status_message) = explode(' ', trim(array_shift($response)), 3);
$result->protocol = $protocol;
$result->status_message = $status_message;
$result->headers = array();
// Parse the response headers.
while ($line = trim(array_shift($response))) {
list($name, $value) = explode(':', $line, 2);
$name = strtolower($name);
if (isset($result->headers[$name]) && $name == 'set-cookie') {
// RFC 2109: the Set-Cookie response header comprises the token Set-
// Cookie:, followed by a comma-separated list of one or more cookies.
$result->headers[$name] .= ',' . trim($value);
}
else {
$result->headers[$name] = trim($value);
}
}
$responses = array(
100 => 'Continue',
101 => 'Switching Protocols',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
307 => 'Temporary Redirect',
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Time-out',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Large',
415 => 'Unsupported Media Type',
416 => 'Requested range not satisfiable',
417 => 'Expectation Failed',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Time-out',
505 => 'HTTP Version not supported',
);
// RFC 2616 states that all unknown HTTP codes must be treated the same as the
// base code in their class.
if (!isset($responses[$code])) {
$code = floor($code / 100) * 100;
}
$result->code = $code;
switch ($code) {
case 200: // OK
case 304: // Not modified
break;
case 301: // Moved permanently
case 302: // Moved temporarily
case 307: // Moved temporarily
$location = $result->headers['location'];
$options['timeout'] -= timer_read(__FUNCTION__) / 1000;
if ($options['timeout'] <= 0) {
$result->code = HTTP_REQUEST_TIMEOUT;
$result->error = 'request timed out';
}
elseif ($options['max_redirects']) {
// Redirect to the new location.
$options['max_redirects']--;
$result = l10n_update_http_request($location, $options);
$result->redirect_code = $code;
}
$result->redirect_url = $location;
break;
default:
$result->error = $status_message;
}
return $result;
}
/**
* Build abstract translation source, to be mapped to a file or a download.
*
* @param $project
* Project object containing data to be inserted in the template.
* @param $template
* String containing place holders. Available place holders:
* - '%project': Project name.
* - '%release': Poject version.
* - '%core': Project core version.
* - '%language': Language code.
* - '%filename': Project file name.
* @return string
* String with replaced place holders.
*/
function l10n_update_build_string($project, $template) {
$variables = array(
'%project' => $project->name,
'%release' => $project->version,
'%core' => $project->core,
'%language' => isset($project->language) ? $project->language : '%language',
'%filename' => isset($project->filename) ? $project->filename : '%filename',
);
return strtr($template, $variables);
}
name = Localization update
description = Provides automatic downloads and updates for translations.
dependencies[] = locale
core = 7.x
package = Multilingual
files[] = l10n_update.admin.inc
files[] = l10n_update.api.php
files[] = l10n_update.batch.inc
files[] = l10n_update.check.inc
files[] = l10n_update.drush.inc
files[] = l10n_update.inc
files[] = l10n_update.install
files[] = l10n_update.locale.inc
files[] = l10n_update.module
files[] = l10n_update.parser.inc
files[] = l10n_update.project.inc
; Information added by drupal.org packaging script on 2012-02-06
version = "7.x-1.0-beta3"
core = "7.x"
project = "l10n_update"
datestamp = "1328563848"
<?php
/**
* @file
* Install file for l10n remote updates.
*/
/**
* Implements hook_schema().
*/
function l10n_update_schema() {
$schema['l10n_update_project'] = array(
'description' => 'Update information for project translations.',
'fields' => array(
'name' => array(
'description' => 'A unique short name to identify the project.',
'type' => 'varchar',
'length' => '50',
'not null' => TRUE,
),
'project_type' => array(
'description' => 'Project type, may be core, module, theme',
'type' => 'varchar',
'length' => '50',
'not null' => TRUE,
),
'core' => array(
'description' => 'Core compatibility string for this project.',
'type' => 'varchar',
'length' => '128',
'not null' => TRUE,
'default' => '',
),
'version' => array(
'description' => 'Human readable name for project used on the interface.',
'type' => 'varchar',
'length' => '128',
'not null' => TRUE,
'default' => '',
),
'l10n_server' => array(
'description' => 'Localization server for this project.',
'type' => 'varchar',
'length' => '255',
'not null' => TRUE,
'default' => '',
),
'l10n_path' => array(
'description' => 'Server path this project updates.',
'type' => 'varchar',
'length' => '255',
'not null' => TRUE,
'default' => '',
),
'status' => array(
'description' => 'Status flag. TBD',
'type' => 'int',
'not null' => TRUE,
'default' => 1,
),
),
'primary key' => array('name'),
);
$schema['l10n_update_file'] = array(
'description' => 'File and download information for project translations.',
'fields' => array(
'project' => array(
'description' => 'A unique short name to identify the project.',
'type' => 'varchar',
'length' => '50',
'not null' => TRUE,
),
'language' => array(
'description' => 'Reference to the {languages}.language for this translation.',
'type' => 'varchar',
'length' => '12',
'not null' => TRUE,
),
'type' => array(
'description' => 'File origin: download or localfile',
'type' => 'varchar',
'length' => '50',
'not null' => TRUE,
'default' => '',
),
'filename' => array(
'description' => 'Link to translation file for download.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
),
'fileurl' => array(
'description' => 'Link to translation file for download.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
),
'uri' => array(
'description' => 'File system path for importing the file.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
),
'timestamp' => array(
'description' => 'Unix timestamp of the time the file was downloaded or saved to disk. Zero if not yet downloaded',
'type' => 'int',
'not null' => FALSE,
'disp-width' => '11',
'default' => 0,
),
'version' => array(
'description' => 'Version tag of the downloaded file.',
'type' => 'varchar',
'length' => '128',
'not null' => TRUE,
'default' => '',
),
'status' => array(
'description' => 'Status flag. TBD',
'type' => 'int',
'not null' => TRUE,
'default' => 1,
),
'last_checked' => array(
'description' => 'Unix timestamp of the last time this translation was downloaded from or checked at remote server and confirmed to be the most recent release available.',
'type' => 'int',
'not null' => FALSE,
'disp-width' => '11',
'default' => 0,
),
),
'primary key' => array('project', 'language'),
);
$schema['cache_l10n_update'] = drupal_get_schema_unprocessed('system', 'cache');
$schema['cache_l10n_update']['description'] = 'Cache table for the Localization Update module to store information about available releases, fetched from central server.';
return $schema;
}
/**
* Implements hook_schema_alter().
*/
function l10n_update_schema_alter(&$schema) {
$schema['locales_target']['fields']['l10n_status'] = array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
);
}
/**
* Implements hook_install().
*/
function l10n_update_install() {
db_add_field('locales_target', 'l10n_status', array('type' => 'int', 'not null' => TRUE, 'default' => 0));
variable_set('l10n_update_rebuild_projects', 1);
}
/**
* Implements hook_uninstall().
*/
function l10n_update_uninstall() {
db_drop_field('locales_target', 'l10n_status');
variable_del('l10n_update_check_disabled');
variable_del('l10n_update_check_frequency');
variable_del('l10n_update_check_mode');
variable_del('l10n_update_default_server');
variable_del('l10n_update_default_update_url');
variable_del('l10n_update_download_store');
variable_del('l10n_update_import_mode');
variable_del('l10n_update_rebuild_projects');
}
/**
* Implements hook_requirements().
*/
function l10n_update_requirements($phase) {
if ($phase == 'runtime') {
if (l10n_update_get_projects() && l10n_update_language_list()) {
$requirements['l10n_update']['title'] = t('Translation update status');
if (l10n_update_available_updates()) {
$requirements['l10n_update']['severity'] = REQUIREMENT_WARNING;
$requirements['l10n_update']['value'] = t('There are available updates');
$requirements['l10n_update']['description'] = t(
'There are new or updated translations available for currently installed modules and themes. To check for updates, you can visit the <a href="@check_manually">translation update page</a>.',
array(
'@check_manually' => url('admin/config/regional/translate/update')
)
);
}
else {
$requirements['l10n_update']['severity'] = REQUIREMENT_OK;
$requirements['l10n_update']['value'] = t('All your translations are up to date');
}
}
else {
$requirements['l10n_update']['title'] = t('Translation update status');
$requirements['l10n_update']['value'] = t('No update data available');
$requirements['l10n_update']['severity'] = REQUIREMENT_WARNING;
//$requirements['update_core']['reason'] = UPDATE_UNKNOWN;
$requirements['l10n_update']['description'] = _l10n_update_no_data();
}
return $requirements;
}
// We must always return array, the installer doesn't use module_invoke_all()
return array();
}
/**
* Add status field to locales_target.
*/
function l10n_update_update_6001() {
if (!db_field_exists('locales_target', 'l10n_status')) {
db_add_field('locales_target', 'l10n_status', array('type' => 'int', 'not null' => TRUE, 'default' => 0));
}
return t('Added l10n_status field to locales_target.');
}
/**
* Change status field name to l10n_status.
*/
function l10n_update_update_6002() {
// I18n Strings module adds a 'status' column to 'locales_target' table.
// L10n Update module previously added a column with the same name. To avoid
// any collision we change the column name here, but only if it was added by
// L10n Update module.
if (!db_field_exists('locales_target', 'l10n_status') && db_field_exists('locales_target', 'status') && !db_table_exists('i18n_strings')) {
db_change_field('locales_target', 'status', 'l10n_status', array('type' => 'int', 'not null' => TRUE, 'default' => 0));
}
// Just in case someone did install I18n Strings, we still need to make sure
// the 'l10n_status' column gets created.
elseif (!db_field_exists('locales_target', 'l10n_status')) {
db_add_field('locales_target', 'l10n_status', array('type' => 'int', 'not null' => TRUE, 'default' => 0));
}
return t('Resolved possible l10n_status field conflict in locales_target.');
}
/**
* Rename filepath to uri in {l10n_update_file} table.
*/
function l10n_update_update_7001() {
// Only do this update if the field exists from D6.
// If it doesn't, we've got a pure D7 site that doesn't need it.
if (db_field_exists('l10n_update_file', 'filepath')) {
db_change_field('l10n_update_file', 'filepath', 'uri', array(
'description' => 'File system path for importing the file.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
));
}
}
/**
* Delete 'last_updated' field from {l10n_update_file} table.
*/
function l10n_update_update_7002() {
db_drop_field('l10n_update_file', 'last_updated');
}
/**
* Delete 'import_date' field from {l10n_update_file} table.
*/
function l10n_update_update_7003() {
db_drop_field('l10n_update_file', 'import_date');
}
/**
* Create {cache_l10n_update} table.
*/
function l10n_update_update_7004() {
if (!db_table_exists('cache_l10n_update')) {
$schema = drupal_get_schema_unprocessed('system', 'cache');
$schema['description'] = 'Cache table for the Localization Update module to store information about available releases, fetched from central server.';
db_create_table('cache_l10n_update', $schema);
}
}
<?php
/**
* @file
* Override part of locale.inc library so we can manage string status
*/
/**
* Parses Gettext Portable Object file information and inserts into database
*
* This is an improved version of _locale_import_po() to handle translation status
*
* @param $file
* Drupal file object corresponding to the PO file to import
* @param $langcode
* Language code
* @param $mode
* Should existing translations be replaced LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE
* @param $group
* Text group to import PO file into (eg. 'default' for interface translations)
*
* @return boolean
* Result array on success. FALSE on failure
*/
function _l10n_update_locale_import_po($file, $langcode, $mode, $group = NULL) {
// Try to allocate enough time to parse and import the data.
drupal_set_time_limit(240);
// Check if we have the language already in the database.
if (!db_query("SELECT COUNT(language) FROM {languages} WHERE language = :language", array(':language' => $langcode))->fetchField()) {
drupal_set_message(t('The language selected for import is not supported.'), 'error');
return FALSE;
}
// Get strings from file (returns on failure after a partial import, or on success)
$status = _l10n_update_locale_import_read_po('db-store', $file, $mode, $langcode, $group);
if ($status === FALSE) {
// Error messages are set in _locale_import_read_po().
return FALSE;
}
// Get status information on import process.
list($header_done, $additions, $updates, $deletes, $skips) = _l10n_update_locale_import_one_string('db-report');
if (!$header_done) {
drupal_set_message(t('The translation file %filename appears to have a missing or malformed header.', array('%filename' => $file->filename)), 'error');
}
// Clear cache and force refresh of JavaScript translations.
_locale_invalidate_js($langcode);
cache_clear_all('locale:', 'cache', TRUE);
// Rebuild the menu, strings may have changed.
menu_rebuild();
watchdog('locale', 'Imported %file into %locale: %number new strings added, %update updated and %delete removed.', array('%file' => $file->filename, '%locale' => $langcode, '%number' => $additions, '%update' => $updates, '%delete' => $deletes));
if ($skips) {
watchdog('locale', '@count disallowed HTML string(s) in %file', array('@count' => $skips, '%file' => $file->uri), WATCHDOG_WARNING);
}
// Return results of this import.
return array(
'file' => $file,
'language' => $langcode,
'add' => $additions,
'update' => $updates,
'delete' => $deletes,
'skip' => $skips,
);
}
/**
* Parses Gettext Portable Object file into an array
*
* @param $op
* Storage operation type: db-store or mem-store
* @param $file
* Drupal file object corresponding to the PO file to import
* @param $mode
* Should existing translations be replaced LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE
* @param $lang
* Language code
* @param $group
* Text group to import PO file into (eg. 'default' for interface translations)
*/
function _l10n_update_locale_import_read_po($op, $file, $mode = NULL, $lang = NULL, $group = 'default') {
$fd = fopen($file->uri, "rb"); // File will get closed by PHP on return
if (!$fd) {
_locale_import_message('The translation import failed, because the file %filename could not be read.', $file);
return FALSE;
}
$context = "COMMENT"; // Parser context: COMMENT, MSGID, MSGID_PLURAL, MSGSTR and MSGSTR_ARR
$current = array(); // Current entry being read
$plural = 0; // Current plural form
$lineno = 0; // Current line
while (!feof($fd)) {
$line = fgets($fd, 10*1024); // A line should not be this long
if ($lineno == 0) {
// The first line might come with a UTF-8 BOM, which should be removed.
$line = str_replace("\xEF\xBB\xBF", '', $line);
}
$lineno++;
$line = trim(strtr($line, array("\\\n" => "")));
if (!strncmp("#", $line, 1)) { // A comment
if ($context == "COMMENT") { // Already in comment context: add
$current["#"][] = substr($line, 1);
}
elseif (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) { // End current entry, start a new one
_l10n_update_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
$current = array();
$current["#"][] = substr($line, 1);
$context = "COMMENT";
}
else { // Parse error
_locale_import_message('The translation file %filename contains an error: "msgstr" was expected but not found on line %line.', $file, $lineno);
return FALSE;
}
}
elseif (!strncmp("msgid_plural", $line, 12)) {
if ($context != "MSGID") { // Must be plural form for current entry
_locale_import_message('The translation file %filename contains an error: "msgid_plural" was expected but not found on line %line.', $file, $lineno);
return FALSE;
}
$line = trim(substr($line, 12));
$quoted = _locale_import_parse_quoted($line);
if ($quoted === FALSE) {
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
return FALSE;
}
$current["msgid"] = $current["msgid"] . "\0" . $quoted;
$context = "MSGID_PLURAL";
}
elseif (!strncmp("msgid", $line, 5)) {
if (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) { // End current entry, start a new one
_l10n_update_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
$current = array();
}
elseif ($context == "MSGID") { // Already in this context? Parse error
_locale_import_message('The translation file %filename contains an error: "msgid" is unexpected on line %line.', $file, $lineno);
return FALSE;
}
$line = trim(substr($line, 5));
$quoted = _locale_import_parse_quoted($line);
if ($quoted === FALSE) {
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
return FALSE;
}
$current["msgid"] = $quoted;
$context = "MSGID";
}
elseif (!strncmp("msgctxt", $line, 7)) {
if (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) { // End current entry, start a new one
_l10n_update_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
$current = array();
}
elseif (!empty($current["msgctxt"])) { // Already in this context? Parse error
_locale_import_message('The translation file %filename contains an error: "msgctxt" is unexpected on line %line.', $file, $lineno);
return FALSE;
}
$line = trim(substr($line, 7));
$quoted = _locale_import_parse_quoted($line);
if ($quoted === FALSE) {
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
return FALSE;
}
$current["msgctxt"] = $quoted;
$context = "MSGCTXT";
}
elseif (!strncmp("msgstr[", $line, 7)) {
if (($context != "MSGID") && ($context != "MSGCTXT") && ($context != "MSGID_PLURAL") && ($context != "MSGSTR_ARR")) { // Must come after msgid, msgxtxt, msgid_plural, or msgstr[]
_locale_import_message('The translation file %filename contains an error: "msgstr[]" is unexpected on line %line.', $file, $lineno);
return FALSE;
}
if (strpos($line, "]") === FALSE) {
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
return FALSE;
}
$frombracket = strstr($line, "[");
$plural = substr($frombracket, 1, strpos($frombracket, "]") - 1);
$line = trim(strstr($line, " "));
$quoted = _locale_import_parse_quoted($line);
if ($quoted === FALSE) {
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
return FALSE;
}
$current["msgstr"][$plural] = $quoted;
$context = "MSGSTR_ARR";
}
elseif (!strncmp("msgstr", $line, 6)) {
if (($context != "MSGID") && ($context != "MSGCTXT")) { // Should come just after a msgid or msgctxt block
_locale_import_message('The translation file %filename contains an error: "msgstr" is unexpected on line %line.', $file, $lineno);
return FALSE;
}
$line = trim(substr($line, 6));
$quoted = _locale_import_parse_quoted($line);
if ($quoted === FALSE) {
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
return FALSE;
}
$current["msgstr"] = $quoted;
$context = "MSGSTR";
}
elseif ($line != "") {
$quoted = _locale_import_parse_quoted($line);
if ($quoted === FALSE) {
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
return FALSE;
}
if (($context == "MSGID") || ($context == "MSGID_PLURAL")) {
$current["msgid"] .= $quoted;
}
elseif ($context == "MSGCTXT") {
$current["msgctxt"] .= $quoted;
}
elseif ($context == "MSGSTR") {
$current["msgstr"] .= $quoted;
}
elseif ($context == "MSGSTR_ARR") {
$current["msgstr"][$plural] .= $quoted;
}
else {
_locale_import_message('The translation file %filename contains an error: there is an unexpected string on line %line.', $file, $lineno);
return FALSE;
}
}
}
// End of PO file, flush last entry.
if (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) {
_l10n_update_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
}
elseif ($context != "COMMENT") {
_locale_import_message('The translation file %filename ended unexpectedly at line %line.', $file, $lineno);
return FALSE;
}
}
/**
* Imports a string into the database
*
* @param $op
* Operation to perform: 'db-store', 'db-report', 'mem-store' or 'mem-report'
* @param $value
* Details of the string stored
* @param $mode
* Should existing translations be replaced LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE
* @param $lang
* Language to store the string in
* @param $file
* Object representation of file being imported, only required when op is 'db-store'
* @param $group
* Text group to import PO file into (eg. 'default' for interface translations)
*/
function _l10n_update_locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NULL, $file = NULL, $group = 'default') {
$report = &drupal_static(__FUNCTION__, array('additions' => 0, 'updates' => 0, 'deletes' => 0, 'skips' => 0));
$header_done = &drupal_static(__FUNCTION__ . ':header_done', FALSE);
$strings = &drupal_static(__FUNCTION__ . ':strings', array());
switch ($op) {
// Return stored strings
case 'mem-report':
return $strings;
// Store string in memory (only supports single strings)
case 'mem-store':
$strings[isset($value['msgctxt']) ? $value['msgctxt'] : ''][$value['msgid']] = $value['msgstr'];
return;
// Called at end of import to inform the user
case 'db-report':
return array($header_done, $report['additions'], $report['updates'], $report['deletes'], $report['skips']);
// Store the string we got in the database.
case 'db-store':
// We got header information.
if ($value['msgid'] == '') {
$languages = language_list();
if (($mode != LOCALE_IMPORT_KEEP) || empty($languages[$lang]->plurals)) {
// Since we only need to parse the header if we ought to update the
// plural formula, only run this if we don't need to keep existing
// data untouched or if we don't have an existing plural formula.
$header = _locale_import_parse_header($value['msgstr']);
// Get the plural formula and update in database.
if (isset($header["Plural-Forms"]) && $p = _locale_import_parse_plural_forms($header["Plural-Forms"], $file->uri)) {
list($nplurals, $plural) = $p;
db_update('languages')
->fields(array(
'plurals' => $nplurals,
'formula' => $plural,
))
->condition('language', $lang)
->execute();
}
else {
db_update('languages')
->fields(array(
'plurals' => 0,
'formula' => '',
))
->condition('language', $lang)
->execute();
}
}
$header_done = TRUE;
}
else {
// Some real string to import.
$comments = _locale_import_shorten_comments(empty($value['#']) ? array() : $value['#']);
if (strpos($value['msgid'], "\0")) {
// This string has plural versions.
$english = explode("\0", $value['msgid'], 2);
$entries = array_keys($value['msgstr']);
for ($i = 3; $i <= count($entries); $i++) {
$english[] = $english[1];
}
$translation = array_map('_locale_import_append_plural', $value['msgstr'], $entries);
$english = array_map('_locale_import_append_plural', $english, $entries);
foreach ($translation as $key => $trans) {
if ($key == 0) {
$plid = 0;
}
$plid = _l10n_update_locale_import_one_string_db($report, $lang, isset($value['msgctxt']) ? $value['msgctxt'] : '', $english[$key], $trans, $group, $comments, $mode, L10N_UPDATE_STRING_DEFAULT, $plid, $key);
}
}
else {
// A simple string to import.
$english = $value['msgid'];
$translation = $value['msgstr'];
_l10n_update_locale_import_one_string_db($report, $lang, isset($value['msgctxt']) ? $value['msgctxt'] : '', $english, $translation, $group, $comments, $mode);
}
}
} // end of db-store operation
}
/**
* Import one string into the database.
*
* @param $report
* Report array summarizing the number of changes done in the form:
* array(inserts, updates, deletes).
* @param $langcode
* Language code to import string into.
* @param $context
* The context of this string.
* @param $source
* Source string.
* @param $translation
* Translation to language specified in $langcode.
* @param $textgroup
* Name of textgroup to store translation in.
* @param $location
* Location value to save with source string.
* @param $mode
* Import mode to use, LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE.
* @param $status
* Status of translation if created: L10N_UPDATE_STRING_DEFAULT or L10N_UPDATE_STRING_CUSTOM
* @param $plid
* Optional plural ID to use.
* @param $plural
* Optional plural value to use.
* @return
* The string ID of the existing string modified or the new string added.
*/
function _l10n_update_locale_import_one_string_db(&$report, $langcode, $context, $source, $translation, $textgroup, $location, $mode, $status = L10N_UPDATE_STRING_DEFAULT, $plid = 0, $plural = 0) {
$lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = :context AND textgroup = :textgroup", array(':source' => $source, ':context' => $context, ':textgroup' => $textgroup))->fetchField();
if (!empty($translation)) {
// Skip this string unless it passes a check for dangerous code.
// Text groups other than default still can contain HTML tags
// (i.e. translatable blocks).
if ($textgroup == "default" && !locale_string_is_safe($translation)) {
$report['skips']++;
$lid = 0;
watchdog('locale', 'Disallowed HTML detected. String not imported: %string', array('%string' => $translation), WATCHDOG_WARNING);
}
elseif ($lid) {
// We have this source string saved already.
db_update('locales_source')
->fields(array(
'location' => $location,
))
->condition('lid', $lid)
->execute();
$exists = db_query("SELECT lid, l10n_status FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $langcode))->fetchObject();
if (!$exists) {
// No translation in this language.
db_insert('locales_target')
->fields(array(
'lid' => $lid,
'language' => $langcode,
'translation' => $translation,
'plid' => $plid,
'plural' => $plural,
))
->execute();
$report['additions']++;
}
elseif (($exists->l10n_status == L10N_UPDATE_STRING_DEFAULT && $mode == LOCALE_UPDATE_OVERRIDE_DEFAULT) || $mode == LOCALE_IMPORT_OVERWRITE) {
// Translation exists, only overwrite if instructed.
db_update('locales_target')
->fields(array(
'translation' => $translation,
'plid' => $plid,
'plural' => $plural,
))
->condition('language', $langcode)
->condition('lid', $lid)
->execute();
$report['updates']++;
}
}
else {
// No such source string in the database yet.
$lid = db_insert('locales_source')
->fields(array(
'location' => $location,
'source' => $source,
'context' => (string) $context,
'textgroup' => $textgroup,
))
->execute();
db_insert('locales_target')
->fields(array(
'lid' => $lid,
'language' => $langcode,
'translation' => $translation,
'plid' => $plid,
'plural' => $plural,
'l10n_status' => $status,
))
->execute();
$report['additions']++;
}
}
elseif ($mode == LOCALE_IMPORT_OVERWRITE) {
// Empty translation, remove existing if instructed.
db_delete('locales_target')
->condition('language', $langcode)
->condition('lid', $lid)
->condition('plid', $plid)
->condition('plural', $plural)
->execute();
$report['deletes']++;
}
return $lid;
}
<?php
/**
* @file
* Download translations from remote localization server.
*
* @todo Fetch information from info files.
*/
/**
* Update mode: Remote server.
*/
define('L10N_UPDATE_CHECK_REMOTE', 1);
/**
* Update mode: Local server.
*/
define('L10N_UPDATE_CHECK_LOCAL', 2);
/**
* Update mode: both.
*/
define('L10N_UPDATE_CHECK_ALL', L10N_UPDATE_CHECK_REMOTE | L10N_UPDATE_CHECK_LOCAL);
/**
* Translation import mode keeping translations which are edited after enabling
* Locale Update module an only override default (un-edited) translations.
*/
define('LOCALE_UPDATE_OVERRIDE_DEFAULT', 2);
/**
* The maximum number of projects which are checked for available translations each cron run.
*/
define('L10N_UPDATE_CRON_PROJECTS', 10);
/**
* The maximum number of projects which are updated each cron run.
*/
define('L10N_UPDATE_CRON_UPDATES', 2);
/**
* Implements hook_help().
*/
function l10n_update_help($path, $arg) {
switch ($path) {
case 'admin/config/regional/translate/update':
$output = '<p>' . t('List of latest imported translations and available updates for each enabled project and language.') . '</p>';
$output .= '<p>' . t('If there are available updates you can click on Update for them to be downloaded and imported now or you can edit the configuration for them to be updated automatically on the <a href="@update-settings">Update settings page</a>', array('@update-settings' => url('admin/config/regional/language/update'))) . '</p>';
return $output;
break;
case 'admin/config/regional/language/update':
$output = '<p>' . t('These are the settings for the translation update system. To update your translations now, check out the <a href="@update-admin">Translation update administration page</a>.', array('@update-admin' => url('admin/config/regional/translate/update'))) . '</p>';
return $output;
break;
}
}
/**
* Implements hook_menu().
*/
function l10n_update_menu() {
$items['admin/config/regional/translate/update'] = array(
'title' => 'Update',
'description' => 'Available updates',
'page callback' => 'l10n_update_admin_overview',
'access arguments' => array('translate interface'),
'file' => 'l10n_update.admin.inc',
'weight' => 20,
'type' => MENU_LOCAL_TASK,
);
$items['admin/config/regional/language/update'] = array(
'title' => 'Translation updates',
'description' => 'Automatic update configuration',
'page callback' => 'drupal_get_form',
'page arguments' => array('l10n_update_admin_settings_form'),
'access arguments' => array('translate interface'),
'file' => 'l10n_update.admin.inc',
'weight' => 20,
'type' => MENU_LOCAL_TASK,
);
return $items;
}
/**
* Implements hook_menu_alter().
*/
function l10n_update_menu_alter(&$menu) {
// Redirect l10n_client AJAX callback path for strings.
$menu['l10n_client/save']['page callback'] = 'l10n_update_client_save_string';
}
/**
* Implements hook_cron().
*
* Check one project/language at a time, download and import if update available
*/
function l10n_update_cron() {
if ($frequency = variable_get('l10n_update_check_frequency', 0)) {
module_load_include('check.inc', 'l10n_update');
list($checked, $updated) = l10n_update_check_translations(L10N_UPDATE_CRON_PROJECTS, REQUEST_TIME - $frequency * 24 * 3600, L10N_UPDATE_CRON_UPDATES);
watchdog('l10n_update', 'Automatically checked @checked translations, updated @updated.', array('@checked' => count($checked), '@updated' => count($updated)));
}
}
/**
* Implements hook_form_alter().
*/
function l10n_update_form_alter(&$form, $form_state, $form_id) {
switch ($form_id) {
case 'locale_translate_edit_form':
// Replace the submit callback by our own customized version
$form['#submit'] = array('l10n_update_locale_translate_edit_form_submit');
break;
case 'locale_languages_predefined_form':
case 'locale_languages_custom_form':
$form['#submit'][] = 'l10n_update_languages_changed_submit';
break;
case 'locale_languages_delete_form':
// A language is being deleted.
$form['#submit'][] = 'l10n_update_languages_delete_submit';
break;
}
}
/**
* Implements hook_modules_enabled().
*
* Refresh project translation status and get translations if required.
*/
function l10n_update_modules_enabled($modules) {
module_load_include('project.inc', 'l10n_update');
l10n_update_project_refresh($modules);
}
/**
* Implements hook_modules_uninstalled().
*
* Remove data of uninstalled modules from {l10n_update_file} table and
* rebuild the projects cache.
*/
function l10n_update_modules_uninstalled($modules) {
db_delete('l10n_update_file')
->condition('project', $modules)
->execute();
// Rebuild {l10n_update_project} table.
// Just like the system table, the project table holds both enabled and
// disabled projects. Full control over its content is not possible.
// To minimize polution we flush it here. The cost of rebuilding is small
// compared to the {l10n_update_file} table.
db_delete('l10n_update_project')->execute();
module_load_include('project.inc', 'l10n_update');
l10n_update_build_projects();
}
/**
* Aditional submit handler for language forms
*
* We need to refresh status when a new language is enabled / disabled
*/
function l10n_update_languages_changed_submit($form, $form_state) {
module_load_include('check.inc', 'l10n_update');
$langcode = $form_state['values']['langcode'];
l10n_update_language_refresh(array($langcode));
}
/**
* Additional submit handler for language deletion form.
*
* When a language is deleted, the file history of this language is cleared.
*/
function l10n_update_languages_delete_submit($form, $form_state) {
$langcode = $form_state['values']['langcode'];
module_load_include('inc', 'l10n_update');
l10n_update_delete_file_history($langcode);
}
/**
* Replacement submit handler for translation edit form.
*
* Process string editing form submissions marking translations as customized.
* Saves all translations of one string submitted from a form.
*
* @see l10n_update_form_alter()
* @todo Just mark as customized when string changed.
*/
function l10n_update_locale_translate_edit_form_submit($form, &$form_state) {
module_load_include('inc', 'l10n_update');
$lid = $form_state['values']['lid'];
foreach ($form_state['values']['translations'] as $key => $value) {
$translation = db_query("SELECT translation FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $key))->fetchField();
if (!empty($value)) {
// Only update or insert if we have a value to use.
if (!empty($translation)) {
db_update('locales_target')
->fields(array(
'translation' => $value,
'l10n_status' => L10N_UPDATE_STRING_CUSTOM,
))
->condition('lid', $lid)
->condition('language', $key)
->execute();
}
else {
db_insert('locales_target')
->fields(array(
'lid' => $lid,
'translation' => $value,
'language' => $key,
'l10n_status' => L10N_UPDATE_STRING_CUSTOM,
))
->execute();
}
}
elseif (!empty($translation)) {
// Empty translation entered: remove existing entry from database.
db_delete('locales_target')
->condition('lid', $lid)
->condition('language', $key)
->execute();
}
// Force JavaScript translation file recreation for this language.
_locale_invalidate_js($key);
}
drupal_set_message(t('The string has been saved.'));
// Clear locale cache.
_locale_invalidate_js();
cache_clear_all('locale:', 'cache', TRUE);
$form_state['redirect'] = 'admin/config/regional/translate/translate';
return;
}
/**
* Menu callback. Saves a string translation coming as POST data.
*/
function l10n_update_client_save_string() {
global $user, $language;
if (l10n_client_access()) {
if (isset($_POST['source']) && isset($_POST['target']) && !empty($_POST['textgroup']) && !empty($_POST['form_token']) && drupal_valid_token($_POST['form_token'], 'l10n_client_form')) {
// Ensure we have this source string before we attempt to save it.
// @todo: add actual context support.
$lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = :context AND textgroup = :textgroup", array(':source' => $_POST['source'], ':context' => '', ':textgroup' => $_POST['textgroup']))->fetchField();
if (!empty($lid)) {
module_load_include('inc', 'l10n_update');
$report = array('skips' => 0, 'additions' => 0, 'updates' => 0, 'deletes' => 0);
// @todo: add actual context support.
_l10n_update_locale_import_one_string_db($report, $language->language, '', $_POST['source'], $_POST['target'], $_POST['textgroup'], NULL, LOCALE_IMPORT_OVERWRITE, L10N_UPDATE_STRING_CUSTOM);
cache_clear_all('locale:', 'cache', TRUE);
_locale_invalidate_js($language->language);
if (!empty($report['skips'])) {
$message = theme('l10n_client_message', array('message' => t('Not saved locally due to invalid HTML content.')));
}
elseif (!empty($report['additions']) || !empty($report['updates'])) {
$message = theme('l10n_client_message', array('message' => t('Translation saved locally.'), 'level' => WATCHDOG_INFO));
}
elseif (!empty($report['deletes'])) {
$message = theme('l10n_client_message', array('message' => t('Translation successfuly removed locally.'), 'level' => WATCHDOG_INFO));
}
else {
$message = theme('l10n_client_message', array('message' => t('Unknown error while saving translation locally.')));
}
// Submit to remote server if enabled.
if (variable_get('l10n_client_use_server', FALSE) && user_access('submit translations to localization server') && ($_POST['textgroup'] == 'default')) {
if (!empty($user->data['l10n_client_key'])) {
$remote_result = l10n_client_submit_translation($language->language, $_POST['source'], $_POST['target'], $user->data['l10n_client_key'], l10n_client_user_token($user));
$message .= theme('l10n_client_message', array('message' => $remote_result[1], 'level' => $remote_result[0] ? WATCHDOG_INFO : WATCHDOG_ERROR));
}
else {
$server_url = variable_get('l10n_client_server', 'http://localize.drupal.org');
$user_edit_url = url('user/' . $user->uid . '/edit', array('absolute' => TRUE));
$message .= theme('l10n_client_message', array('message' => t('You could share your work with !l10n_server if you set your API key at !user_link.', array('!l10n_server' => l($server_url, $server_url), '!user_link' => l($user_edit_url, 'user/' . $user->uid . '/edit'))), 'level' => WATCHDOG_WARNING));
}
}
}
else {
$message = theme('l10n_client_message', array('message' => t('Not saved due to source string missing.')));
}
}
else {
$message = theme('l10n_client_message', array('message' => t('Not saved due to missing form values.')));
}
}
else {
$message = theme('l10n_client_message', array('message' => t('Not saved due to insufficient permissions.')));
}
drupal_json_output($message);
exit;
}
/**
* Get stored list of projects
*
* @param boolean $refresh
* TRUE = refresh the project data.
* @param boolean $disabled
* TRUE = get enabled AND disabled projects.
* FALSE = get enabled projects only.
*
* @return array
* Array of project objects keyed by project name.
*/
function l10n_update_get_projects($refresh = FALSE, $disabled = FALSE) {
static $projects, $enabled;
if (!isset($projects) || $refresh) {
if (variable_get('l10n_update_rebuild_projects', 0)) {
module_load_include('project.inc', 'l10n_update');
variable_del('l10n_update_rebuild_projects');
l10n_update_build_projects();
}
$projects = $enabled = array();
$result = db_query('SELECT * FROM {l10n_update_project}');
foreach ($result as $project) {
$projects[$project->name] = $project;
if ($project->status) {
$enabled[$project->name] = $project;
}
}
}
return $disabled ? $projects : $enabled;
}
/**
* Get server information, that can come from different sources.
*
* - From server list provided by modules. They can provide full server information or just the url
* - From server_url in a project, we'll fetch latest data from the server itself
*
* @param string $name
* Server name e.g. localize.drupal.org
* @param string $url
* Server url
* @param boolean $refresh
* TRUE = refresh the server data.
*
* @return array
* Array of server data.
*/
function l10n_update_server($name = NULL, $url = NULL, $refresh = FALSE) {
static $info, $server_list;
// Retrieve server list from modules
if (!isset($server_list) || $refresh) {
$server_list = module_invoke_all('l10n_servers');
}
// We need at least the server url to fetch all the information
if (!$url && $name && isset($server_list[$name])) {
$url = $server_list[$name]['server_url'];
}
// If we still don't have an url, cannot find this server, return false
if (!$url) {
return FALSE;
}
// Cache server information based on the url, refresh if asked
$cid = 'l10n_update_server:' . $url;
if ($refresh) {
unset($info);
cache_clear_all($cid, 'cache_l10n_update');
}
if (!isset($info[$url])) {
if ($cache = cache_get($cid, 'cache_l10n_update')) {
$info[$url] = $cache->data;
}
else {
module_load_include('parser.inc', 'l10n_update');
if ($name && !empty($server_list[$name])) {
// The name is in our list, it can be full data or just an url
$server = $server_list[$name];
}
else {
// This may be a new server provided by a module / package
$server = array('name' => $name, 'server_url' => $url);
// If searching by name, store the name => url mapping
if ($name) {
$server_list[$name] = $server;
}
}
// Now fetch server meta information form the server itself
if ($server = l10n_update_get_server($server)) {
cache_set($cid, $server, 'cache_l10n_update');
$info[$url] = $server;
}
else {
// If no server information, this will be FALSE. We won't search a server twice
$info[$url] = FALSE;
}
}
}
return $info[$url];
}
/**
* Implements hook_l10n_servers().
*
* @return array
* Array of server data:
* 'name' => server name
* 'server_url' => server url
* 'update_url' => update url
*/
function l10n_update_l10n_servers() {
module_load_include('inc', 'l10n_update');
$server = l10n_update_default_server();
return array($server['name'] => $server);
}
/**
* Get update history.
*
* @param boolean $refresh
* TRUE = refresh the history data.
* @return
* An array of translation files indexed by project and language.
*/
function l10n_update_get_history($refresh = NULL) {
static $status;
if ($refresh || !isset($status)) {
// Now add downloads history to projects
$result = db_query("SELECT * FROM {l10n_update_file}");
foreach ($result as $update) {
$status[$update->project][$update->language] = $update;
}
}
return $status;
}
/**
* Get language list.
*
* @return array
* Array of installed language names. English is the source language and
* is therefore not included.
*/
function l10n_update_language_list() {
$languages = locale_language_list('name');
// Skip English language
if (isset($languages['en'])) {
unset($languages['en']);
}
return $languages;
}
/**
* Implements hook_theme().
*/
function l10n_update_theme() {
return array(
'l10n_update_project_status' => array(
'variables' => array('projects' => NULL, 'languages' => NULL, 'history' => NULL, 'available' => NULL, 'updates' => NULL),
'file' => 'l10n_update.admin.inc',
),
'l10n_update_single_project_wrapper' => array(
'project' => array('project' => NULL, 'project_status' => NULL, 'languages' => NULL, 'history' => NULL, 'updates' => NULL),
'file' => 'l10n_update.admin.inc',
),
'l10n_update_single_project_status' => array(
'variables' => array('project' => NULL, 'server' => NULL, 'status' => NULL),
'file' => 'l10n_update.admin.inc',
),
'l10n_update_current_release' => array(
'variables' => array('language' => NULL, 'release' => NULL, 'status' => NULL),
'file' => 'l10n_update.admin.inc',
),
'l10n_update_available_release' => array(
'variables' => array('release' => NULL),
'file' => 'l10n_update.admin.inc',
),
'l10n_update_version_status' => array(
'variables' => array('status' => NULL, 'type' => NULL),
'file' => 'l10n_update.admin.inc',
),
);
}
/**
* Build the warning message for when there is no data about available updates.
*
* @return sting
* Message text with links.
*/
function _l10n_update_no_data() {
$destination = drupal_get_destination();
return t('No information is available about potential new and updated translations for currently installed modules and themes. To check for updates, you may need to <a href="@run_cron">run cron</a> or you can <a href="@check_manually">check manually</a>. Please note that checking for available updates can take a long time, so please be patient.', array(
'@run_cron' => url('admin/reports/status/run-cron', array('query' => $destination)),
'@check_manually' => url('admin/config/regional/translate/update', array('query' => $destination)),
));
}
/**
* Get available updates.
*
* @param boolean $refresh
* TRUE = refresh the history data.
*
* @return array
* Array of all projects for which updates are available. For each project
* an array of update objects, one per language.
*/
function l10n_update_available_updates($refresh = NULL) {
module_load_include('check.inc', 'l10n_update');
if ($available = l10n_update_available_releases($refresh)) {
$history = l10n_update_get_history();
return l10n_update_build_updates($history, $available);
}
}
/**
* Implements hook_flush_caches().
*
* Called from update.php (among others) to flush the caches.
*/
function l10n_update_flush_caches() {
if (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update') {
cache_clear_all('*', 'cache_l10n_update', TRUE);
variable_set('l10n_update_rebuild_projects', 1);
}
return array();
}
<?php
/**
* @file
* Extends the update parser to work with releases
*
* The update parser uses version tag to index releases. We will use 'language' and 'tag'
*
* @todo Parse languages too
*
* @todo Update the server side and get rid of this
*/
module_load_include('inc', 'l10n_update');
/**
* Get server information
*/
function l10n_update_get_server($server) {
// Fetch up to date information if available
if (!empty($server['server_url']) && ($fetch = l10n_update_fetch_server($server['server_url']))) {
$server = array_merge($server, $fetch);
}
// If we have an update url this is ok, otherwise we return none
if (!empty($server['update_url'])) {
return $server;
}
else {
return FALSE;
}
}
/**
* Fetch remote server metadata from a server URL
*
* @param unknown_type $server_url
* @return unknown_type
*/
function l10n_update_fetch_server($url) {
$xml = l10n_update_http_request($url);
if (isset($xml->data)) {
$data[] = $xml->data;
$parser = new l10n_update_xml_parser;
return $parser->parse($xml->data);
}
else {
return FALSE;
}
}
/**
* Parser for server metadata
*/
class l10n_update_xml_parser {
var $current_language;
var $current_server;
var $current_languages;
var $servers;
/**
* Parse an XML data file.
*
* It can contain information for one or more l10n_servers
*
* Example data, http://ftp.drupal.org/files/translations/l10n_server.xml
*/
function parse($data) {
$parser = xml_parser_create();
xml_set_object($parser, $this);
xml_set_element_handler($parser, 'start', 'end');
xml_set_character_data_handler($parser, "data");
xml_parse($parser, $data);
xml_parser_free($parser);
//return $this->servers;
return $this->current_server;
}
function start($parser, $name, $attr) {
$this->current_tag = $name;
switch ($name) {
case 'L10N_SERVER':
unset($this->current_object);
$this->current_server = array();
$this->current_object = &$this->current_server;
break;
case 'LANGUAGES':
unset($this->current_object);
$this->current_languages = array();
$this->current_object = &$this->current_languages;
//$this->current_object = &$this->current_release;
break;
case 'LANGUAGE':
unset($this->current_object);
$this->current_language = array();
$this->current_object = &$this->current_language;
break;
}
}
function end($parser, $name) {
switch ($name) {
case 'L10N_SERVER':
unset($this->current_object);
$this->servers[$this->current_server['name']] = $this->current_server;
//$this->current_server = array();
break;
case 'LANGUAGE':
unset($this->current_object);
$this->current_languages[$this->current_language['code']] = $this->current_language;
$this->current_language = array();
break;
case 'LANGUAGES':
$this->current_server['languages'] = $this->current_languages;
break;
default:
$this->current_object[strtolower($this->current_tag)] = trim($this->current_object[strtolower($this->current_tag)]);
$this->current_tag = '';
}
}
function data($parser, $data) {
if ($this->current_tag && !in_array($this->current_tag, array('L10N_SERVER', 'LANGUAGES', 'LANGUAGE'))) {
$tag = strtolower($this->current_tag);
if (isset($this->current_object[$tag])) {
$this->current_object[$tag] .= $data;
}
else {
$this->current_object[$tag] = $data;
}
}
}
}
<?php
/**
* @file
* Library for querying Drupal projects
*
* Most code is taken from update module. We don't want to depend on it though as it may not be enabled.
*
* For each project, the information about where to fetch translations may be specified in the info files
* as follows:
*
* - Localization server to be used for this project. Defaults to http://localize.drupal.org
* l10n server = localize.drupal.org
* (This should be enough if the server url, the one below, is defined somewhere else)
*
* - Metadata information for the localization server
* l10n url = http://ftp.drupal.org/files/translations/l10n_server.xml
* (We can fetch *all* the information we need from this single url)
*
* - Translation file URL template, will be used to build the file url to download
* l10n path = http://ftp.drupal.org/files/translations/%core/%project/%project-%release.%language.po
* (Alternatively you can use the %filename variable that will default to '%project-%release.%language.po')
*/
/**
* Rebuild project list
*
* @return array
*/
function l10n_update_build_projects() {
module_load_include('inc', 'l10n_update');
// Get all stored projects, including disabled ones
$current = l10n_update_get_projects(NULL, TRUE);
// Now get the new project list, just enabled ones
$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_server();
if (module_exists('update')) {
$projects_info = update_get_available(TRUE);
}
foreach ($projects as $name => $data) {
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;
}
}
}
elseif ($name == "drupal" || preg_match("/HEAD/", $data['info']['version'], $matches)) {
// Pick latest available release.
$release = array_shift($projects_info[$name]['releases']);
}
if (!empty($release['version'])) {
$data['info']['version'] = $release['version'];
}
unset($release);
}
$data += array(
'version' => isset($data['info']['version']) ? $data['info']['version'] : '',
'core' => isset($data['info']['core']) ? $data['info']['core'] : DRUPAL_CORE_COMPATIBILITY,
// The project can have its own l10n server, we use default if not
'l10n_server' => isset($data['info']['l10n server']) ? $data['info']['l10n server'] : NULL,
// A project can provide the server url to fetch metadata, or the update url (path)
'l10n_url' => isset($data['info']['l10n url']) ? $data['info']['l10n url'] : NULL,
'l10n_path' => isset($data['info']['l10n path']) ? $data['info']['l10n path'] : NULL,
'status' => 1,
);
$project = (object) $data;
// Unless the project provides a full l10n path (update url), we try to build one
if (!isset($project->l10n_path)) {
$server = NULL;
if ($project->l10n_server || $project->l10n_url) {
$server = l10n_update_server($project->l10n_server, $project->l10n_url);
}
else {
// Use the default server
$server = l10n_update_server($default_server['name'], $default_server['server_url']);
}
if ($server) {
// Build the update path for this project, with project name and release replaced
$project->l10n_path = l10n_update_build_string($project, $server['update_url']);
}
}
// Create / update project record
$update = empty($current[$name]) ? array() : array('name');
drupal_write_record('l10n_update_project', $project, $update);
$projects[$name] = $project;
}
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;
}
/**
* Refresh projects after enabling modules
*
* When new projects are installed, set a batch for locale import / update
*
* @param $modules
* Array of module names.
*/
function l10n_update_project_refresh($modules) {
module_load_include('check.inc', 'l10n_update');
$projects = array();
// Get all current projects, including the recently installed.
$current_projects = l10n_update_build_projects();
// Collect project data of newly installed projects.
foreach ($modules as $name) {
if (isset($current_projects[$name])) {
$projects[$name] = $current_projects[$name];
}
}
// If a translation is available and if update is required, lets go.
if ($projects && $available = l10n_update_check_projects($projects)) {
$history = l10n_update_get_history();
if ($updates = l10n_update_build_updates($history, $available)) {
module_load_include('batch.inc', 'l10n_update');
// Filter out updates in other languages. If no languages, all of them will be updated
$updates = _l10n_update_prepare_updates($updates);
$batch = l10n_update_batch_multiple($updates, variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP));
batch_set($batch);
}
}
}
/**
* 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 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 => $file->info['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;
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment