non security modules update

This commit is contained in:
Bachir Soussi Chiadmi
2015-04-20 16:32:07 +02:00
parent 6a8d30db08
commit 37fbabab56
466 changed files with 32690 additions and 9652 deletions

View File

@@ -26,42 +26,43 @@ 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"
You need at least one language (besides the default English).
On Administration > Configuration > Regional and language > Languages:
Click "Add language".
Select a language from the select list "Language name".
Then click the "Add language" button.
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.
When it's finished, you'll get a confirmation with a summary of the
translations that have been imported.
If required, enable the new language as default language.
Home > Administration > Configuration > Regional and language:
Select your new language as default
Administration > Configuration > Regional and language > Languages:
Select your new default language.
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".
You want to import translations regularly using cron. You can enable this
on Administration > Configuration > Regional and language > Languages:
Choose the "Translation updates" tab.
Change "Check for updates" to "Daily" or "Weekly" instead of the default "Never".
From now on cron will check for updated translations, and import them is required.
Cron will from now on check for updated translations, and will report the
update status on the status page (Home > Administration > Reports).
The status of the translations is reported on the "Status report" page at
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
Administration > Configuration > Regional and language > Translate inteface
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
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
@@ -114,27 +115,37 @@ po files, multi site and distributions
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
Alternative source of translation
---------------------------------
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:
You may override the download path of the po file on a project by project
basis by adding this definition in the .info file:
l10n path = http://example.com/files/translations/%core/%project/%project-%release.%language.po
Modules can force Locale to load the translation of an other project by
defining 'interface translation project' in their .info file. This can be
usefull for custom modules to use for example a common translation file
interface translation project = my_project
This can be used in combination with an alternative path to the translation
file. For example:
l10n path = sites/all/modules/custom/%project/%project.%language.po
Exclude a project from translation checks and updates
-----------------------------------------------------
Individual modules can be excluded from translation checks and updates. For
example custom modules or features. Add the following line to the .info file
to exclude a module from translation checks and updates:
interface translation project = FALSE
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.
@@ -142,6 +153,6 @@ API
Maintainers
-----------
Jose Reyero
Gábor Hojtsy
Erik Stielstra
Gábor Hojtsy
Jose Reyero

View File

@@ -1,25 +1,11 @@
/* Expand/collapse image for project title */
html.js .l10n-update-wrapper .project-legend {
padding-right: 10px;
/**
* Available translation updates page.
*/
#l10n-update-status-form .expand .inner {
background: transparent url(../images/menu-collapsed-rtl.png) right .6em no-repeat;
margin-right: -12px;
padding-right: 12px;
}
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;
#l10n-update-status-form .expanded .expand .inner {
background: transparent url(../images/menu-expanded.png) right .6em no-repeat;
}

View File

@@ -1,46 +1,82 @@
html.js .l10n-update-wrapper.collapsed .fieldset-wrapper {
display: none;
/**
* Available translation updates page.
*/
#l10n-update-status-form table {
table-layout: fixed;
}
#l10n-update-status-form th.select-all {
width: 4%;
}
#l10n-update-status-form th.title {
width: 25%;
}
#l10n-update-status-form th.description {
}
#l10n-update-status-form td {
vertical-align: top;
}
#l10n-update-status-form .expand .inner {
background: transparent url(../images/menu-collapsed.png) left .6em no-repeat;
margin-left: -12px;
padding-left: 12px;
}
#l10n-update-status-form .expanded .expand .inner {
background: transparent url(../images/menu-expanded.png) left .6em no-repeat;
}
.l10n-update-wrapper .project .version-status {
display: none;
#l10n-update-status-form .label {
color: #1d1d1d;
font-size: 1.15em;
font-weight: bold;
}
/* 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;
#l10n-update-status-form .description {
cursor: pointer;
}
html.js .l10n-update-wrapper.collapsed .project-title {
background: url("../images/menu-collapsed.png") left 50% no-repeat;
#l10n-update-status-form .description .inner {
color: #5c5c5b;
line-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
html.js .l10n-update-wrapper .project-title a {
margin-left: -10px;
padding-left: 10px;
#l10n-update-status-form .expanded .description .inner {
height: auto;
overflow: visible;
white-space: normal;
}
/* Translation update status data */
html.js .l10n-update-wrapper.collapsed .project .version-status {
display: inline;
float: right;
#l10n-update-status-form .expanded .description .text {
-webkit-hyphens: auto;
-moz-hyphens: auto;
hyphens: auto;
}
.l10n-update .project-server {
margin: 0 10px;
font-size: 90%;
font-weight: normal
.js #l10n-update-status-form .description .inner {
height: 20px;
}
.l10n-update .version-status {
float: right;
font-size: 90%;
font-weight: normal;
#l10n-update-status-form .expanded .description .inner {
height: auto;
overflow: visible;
white-space: normal;
}
.l10n-update .version-links {
float: right;
padding-right: 1em;
#l10n-update-status-form .details {
padding: 5px 0;
max-width: 490px;
white-space: normal;
font-size: 0.9em;
color: #666;
}
#l10n-update-status-form .visually-hidden {
position: absolute !important;
clip: rect(1px, 1px, 1px, 1px);
overflow: hidden;
height: 1px;
width: 1px;
word-wrap: normal;
}
@media screen and (max-width: 40em) {
#l10n-update-status-form th.title {
width: 20%;
}
#l10n-update-status-form th.status {
width: 40%;
}
}

View File

@@ -0,0 +1,418 @@
<?php
/**
* @file
* Definition of Drupal\Component\Gettext\PoHeader
*/
/**
* Gettext PO header handler.
*
* Possible Gettext PO header elements are explained in
* http://www.gnu.org/software/gettext/manual/gettext.html#Header-Entry,
* but we only support a subset of these directly.
*
* Example header:
*
* "Project-Id-Version: Drupal core (7.11)\n"
* "POT-Creation-Date: 2012-02-12 22:59+0000\n"
* "PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\n"
* "Language-Team: Catalan\n"
* "MIME-Version: 1.0\n"
* "Content-Type: text/plain; charset=utf-8\n"
* "Content-Transfer-Encoding: 8bit\n"
* "Plural-Forms: nplurals=2; plural=(n>1);\n"
*/
class PoHeader {
/**
* Language code.
*
* @var string
*/
private $_langcode;
/**
* Formula for the plural form.
*
* @var string
*/
private $_pluralForms;
/**
* Author(s) of the file.
*
* @var string
*/
private $_authors;
/**
* Date the po file got created.
*
* @var string
*/
private $_po_date;
/**
* Human readable language name.
*
* @var string
*/
private $_languageName;
/**
* Name of the project the translation belongs to.
*
* @var string
*/
private $_projectName;
/**
* Constructor, creates a PoHeader with default values.
*
* @param string $langcode
* Language code.
*/
public function __construct($langcode = NULL) {
$this->_langcode = $langcode;
// Ignore errors when run during site installation before
// date_default_timezone_set() is called.
$this->_po_date = @date("Y-m-d H:iO");
$this->_pluralForms = 'nplurals=2; plural=(n > 1);';
}
/**
* Get the plural form.
*
* @return string
* Plural form component from the header, for example:
* 'nplurals=2; plural=(n > 1);'.
*/
function getPluralForms() {
return $this->_pluralForms;
}
/**
* Set the human readable language name.
*
* @param string $languageName
* Human readable language name.
*/
function setLanguageName($languageName) {
$this->_languageName = $languageName;
}
/**
* Get the human readable language name.
*
* @return string
* The human readable language name.
*/
function getLanguageName() {
return $this->_languageName;
}
/**
* Set the project name.
*
* @param string $projectName
* Human readable project name.
*/
function setProjectName($projectName) {
$this->_projectName = $projectName;
}
/**
* Get the project name.
*
* @return string
* The human readable project name.
*/
function getProjectName() {
return $this->_projectName;
}
/**
* Populate internal values from a string.
*
* @param string $header
* Full header string with key-value pairs.
*/
public function setFromString($header) {
// Get an array of all header values for processing.
$values = $this->parseHeader($header);
// There is only one value relevant for our header implementation when
// reading, and that is the plural formula.
if (!empty($values['Plural-Forms'])) {
$this->_pluralForms = $values['Plural-Forms'];
}
}
/**
* Generate a Gettext PO formatted header string based on data set earlier.
*/
public function __toString() {
$output = '';
$isTemplate = empty($this->_languageName);
$output .= '# ' . ($isTemplate ? 'LANGUAGE' : $this->_languageName) . ' translation of ' . ($isTemplate ? 'PROJECT' : $this->_projectName) . "\n";
if (!empty($this->_authors)) {
$output .= '# Generated by ' . implode("\n# ", $this->_authors) . "\n";
}
$output .= "#\n";
// Add the actual header information.
$output .= "msgid \"\"\n";
$output .= "msgstr \"\"\n";
$output .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n";
$output .= "\"POT-Creation-Date: " . $this->_po_date . "\\n\"\n";
$output .= "\"PO-Revision-Date: " . $this->_po_date . "\\n\"\n";
$output .= "\"Last-Translator: NAME <EMAIL@ADDRESS>\\n\"\n";
$output .= "\"Language-Team: LANGUAGE <EMAIL@ADDRESS>\\n\"\n";
$output .= "\"MIME-Version: 1.0\\n\"\n";
$output .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n";
$output .= "\"Content-Transfer-Encoding: 8bit\\n\"\n";
$output .= "\"Plural-Forms: " . $this->_pluralForms . "\\n\"\n";
$output .= "\n";
return $output;
}
/**
* Parses a Plural-Forms entry from a Gettext Portable Object file header.
*
* @param string $pluralforms
* The Plural-Forms entry value.
*
* @return
* An array containing the number of plural forms and the converted version
* of the formula that can be evaluated with PHP later.
*/
function parsePluralForms($pluralforms) {
// First, delete all whitespace.
$pluralforms = strtr($pluralforms, array(" " => "", "\t" => ""));
// Select the parts that define nplurals and plural.
$nplurals = strstr($pluralforms, "nplurals=");
if (strpos($nplurals, ";")) {
// We want the string from the 10th char, because "nplurals=" length is 9.
$nplurals = substr($nplurals, 9, strpos($nplurals, ";") - 9);
}
else {
return FALSE;
}
$plural = strstr($pluralforms, "plural=");
if (strpos($plural, ";")) {
// We want the string from the 8th char, because "plural=" length is 7.
$plural = substr($plural, 7, strpos($plural, ";") - 7);
}
else {
return FALSE;
}
// Get PHP version of the plural formula.
$plural = $this->parseArithmetic($plural);
if ($plural !== FALSE) {
return array($nplurals, $plural);
}
else {
throw new Exception('The plural formula could not be parsed.');
}
}
/**
* Parses a Gettext Portable Object file header.
*
* @param string $header
* A string containing the complete header.
*
* @return array
* An associative array of key-value pairs.
*/
private function parseHeader($header) {
$header_parsed = array();
$lines = array_map('trim', explode("\n", $header));
foreach ($lines as $line) {
if ($line) {
list($tag, $contents) = explode(":", $line, 2);
$header_parsed[trim($tag)] = trim($contents);
}
}
return $header_parsed;
}
/**
* Parses and sanitizes an arithmetic formula into a PHP expression.
*
* While parsing, we ensure, that the operators have the right
* precedence and associativity.
*
* @param string $string
* A string containing the arithmetic formula.
*
* @return
* A version of the formula to evaluate with PHP later.
*/
private function parseArithmetic($string) {
// Operator precedence table.
$precedence = array("(" => -1, ")" => -1, "?" => 1, ":" => 1, "||" => 3, "&&" => 4, "==" => 5, "!=" => 5, "<" => 6, ">" => 6, "<=" => 6, ">=" => 6, "+" => 7, "-" => 7, "*" => 8, "/" => 8, "%" => 8);
// Right associativity.
$right_associativity = array("?" => 1, ":" => 1);
$tokens = $this->tokenizeFormula($string);
// Parse by converting into infix notation then back into postfix
// Operator stack - holds math operators and symbols.
$operator_stack = array();
// Element Stack - holds data to be operated on.
$element_stack = array();
foreach ($tokens as $token) {
$current_token = $token;
// Numbers and the $n variable are simply pushed into $element_stack.
if (is_numeric($token)) {
$element_stack[] = $current_token;
}
elseif ($current_token == "n") {
$element_stack[] = '$n';
}
elseif ($current_token == "(") {
$operator_stack[] = $current_token;
}
elseif ($current_token == ")") {
$topop = array_pop($operator_stack);
while (isset($topop) && ($topop != "(")) {
$element_stack[] = $topop;
$topop = array_pop($operator_stack);
}
}
elseif (!empty($precedence[$current_token])) {
// If it's an operator, then pop from $operator_stack into
// $element_stack until the precedence in $operator_stack is less
// than current, then push into $operator_stack.
$topop = array_pop($operator_stack);
while (isset($topop) && ($precedence[$topop] >= $precedence[$current_token]) && !(($precedence[$topop] == $precedence[$current_token]) && !empty($right_associativity[$topop]) && !empty($right_associativity[$current_token]))) {
$element_stack[] = $topop;
$topop = array_pop($operator_stack);
}
if ($topop) {
// Return element to top.
$operator_stack[] = $topop;
}
// Parentheses are not needed.
$operator_stack[] = $current_token;
}
else {
return FALSE;
}
}
// Flush operator stack.
$topop = array_pop($operator_stack);
while ($topop != NULL) {
$element_stack[] = $topop;
$topop = array_pop($operator_stack);
}
// Now extract formula from stack.
$previous_size = count($element_stack) + 1;
while (count($element_stack) < $previous_size) {
$previous_size = count($element_stack);
for ($i = 2; $i < count($element_stack); $i++) {
$op = $element_stack[$i];
if (!empty($precedence[$op])) {
$f = "";
if ($op == ":") {
$f = $element_stack[$i - 2] . "):" . $element_stack[$i - 1] . ")";
}
elseif ($op == "?") {
$f = "(" . $element_stack[$i - 2] . "?(" . $element_stack[$i - 1];
}
else {
$f = "(" . $element_stack[$i - 2] . $op . $element_stack[$i - 1] . ")";
}
array_splice($element_stack, $i - 2, 3, $f);
break;
}
}
}
// If only one element is left, the number of operators is appropriate.
if (count($element_stack) == 1) {
return $element_stack[0];
}
else {
return FALSE;
}
}
/**
* Tokenize the formula.
*
* @param string $formula
* A string containing the arithmetic formula.
*
* @return array
* List of arithmetic tokens identified in the formula.
*/
private function tokenizeFormula($formula) {
$formula = str_replace(" ", "", $formula);
$tokens = array();
for ($i = 0; $i < strlen($formula); $i++) {
if (is_numeric($formula[$i])) {
$num = $formula[$i];
$j = $i + 1;
while ($j < strlen($formula) && is_numeric($formula[$j])) {
$num .= $formula[$j];
$j++;
}
$i = $j - 1;
$tokens[] = $num;
}
elseif ($pos = strpos(" =<>!&|", $formula[$i])) {
$next = $formula[$i + 1];
switch ($pos) {
case 1:
case 2:
case 3:
case 4:
if ($next == '=') {
$tokens[] = $formula[$i] . '=';
$i++;
}
else {
$tokens[] = $formula[$i];
}
break;
case 5:
if ($next == '&') {
$tokens[] = '&&';
$i++;
}
else {
$tokens[] = $formula[$i];
}
break;
case 6:
if ($next == '|') {
$tokens[] = '||';
$i++;
}
else {
$tokens[] = $formula[$i];
}
break;
}
}
else {
$tokens[] = $formula[$i];
}
}
return $tokens;
}
}

View File

@@ -0,0 +1,282 @@
<?php
/**
* @file
* Definition of Drupal\Component\Gettext\PoItem.
*/
/**
* PoItem handles one translation.
*/
class PoItem {
/**
* The language code this translation is in.
*
* @car string
*/
private $_langcode;
/**
* The context this translation belongs to.
*
* @var string
*/
private $_context = '';
/**
* The source string or array of strings if it has plurals.
*
* @var string or array
* @see $_plural
*/
private $_source;
/**
* Flag indicating if this translation has plurals.
*
* @var boolean
*/
private $_plural;
/**
* The comment of this translation.
*
* @var string
*/
private $_comment;
/**
* The translation string or array of strings if it has plurals.
*
* @var string or array
* @see $_plural
*/
private $_translation;
/**
* Get the language code of the currently used language.
*
* @return string with langcode
*/
function getLangcode() {
return $this->_langcode;
}
/**
* Set the language code of the current language.
*
* @param string $langcode
*/
function setLangcode($langcode) {
$this->_langcode = $langcode;
}
/**
* Get the context this translation belongs to.
*
* @return string $context
*/
function getContext() {
return $this->_context;
}
/**
* Set the context this translation belongs to.
*
* @param string $context
*/
function setContext($context) {
$this->_context = $context;
}
/**
* Get the source string or the array of strings if the translation has
* plurals.
*
* @return string or array $translation
*/
function getSource() {
return $this->_source;
}
/**
* Set the source string or the array of strings if the translation has
* plurals.
*
* @param string or array $source
*/
function setSource($source) {
$this->_source = $source;
}
/**
* Get the translation string or the array of strings if the translation has
* plurals.
*
* @return string or array $translation
*/
function getTranslation() {
return $this->_translation;
}
/**
* Set the translation string or the array of strings if the translation has
* plurals.
*
* @param string or array $translation
*/
function setTranslation($translation) {
$this->_translation = $translation;
}
/**
* Set if the translation has plural values.
*
* @param boolean $plural
*/
function setPlural($plural) {
$this->_plural = $plural;
}
/**
* Get if the translation has plural values.
*
* @return boolean $plural
*/
function isPlural() {
return $this->_plural;
}
/**
* Get the comment of this translation.
*
* @return String $comment
*/
function getComment() {
return $this->_comment;
}
/**
* Set the comment of this translation.
*
* @param String $comment
*/
function setComment($comment) {
$this->_comment = $comment;
}
/**
* Create the PoItem from a structured array.
*
* @param array values
*/
public function setFromArray(array $values = array()) {
if (isset($values['context'])) {
$this->setContext($values['context']);
}
if (isset($values['source'])) {
$this->setSource($values['source']);
}
if (isset($values['translation'])) {
$this->setTranslation($values['translation']);
}
if (isset($values['comment'])){
$this->setComment($values['comment']);
}
if (isset($this->_source) &&
strpos($this->_source, L10N_UPDATE_PLURAL_DELIMITER) !== FALSE) {
$this->setSource(explode(L10N_UPDATE_PLURAL_DELIMITER, $this->_source));
$this->setTranslation(explode(L10N_UPDATE_PLURAL_DELIMITER, $this->_translation));
$this->setPlural(count($this->_translation) > 1);
}
}
/**
* Output the PoItem as a string.
*/
public function __toString() {
return $this->formatItem();
}
/**
* Format the POItem as a string.
*/
private function formatItem() {
$output = '';
// Format string context.
if (!empty($this->_context)) {
$output .= 'msgctxt ' . $this->formatString($this->_context);
}
// Format translation.
if ($this->_plural) {
$output .= $this->formatPlural();
}
else {
$output .= $this->formatSingular();
}
// Add one empty line to separate the translations.
$output .= "\n";
return $output;
}
/**
* Formats a plural translation.
*/
private function formatPlural() {
$output = '';
// Format source strings.
$output .= 'msgid ' . $this->formatString($this->_source[0]);
$output .= 'msgid_plural ' . $this->formatString($this->_source[1]);
foreach ($this->_translation as $i => $trans) {
if (isset($this->_translation[$i])) {
$output .= 'msgstr[' . $i . '] ' . $this->formatString($trans);
}
else {
$output .= 'msgstr[' . $i . '] ""' . "\n";
}
}
return $output;
}
/**
* Formats a singular translation.
*/
private function formatSingular() {
$output = '';
$output .= 'msgid ' . $this->formatString($this->_source);
$output .= 'msgstr ' . (isset($this->_translation) ? $this->formatString($this->_translation) : '""');
return $output;
}
/**
* Formats a string for output on multiple lines.
*/
private function formatString($string) {
// Escape characters for processing.
$string = addcslashes($string, "\0..\37\\\"");
// Always include a line break after the explicit \n line breaks from
// the source string. Otherwise wrap at 70 chars to accommodate the extra
// format overhead too.
$parts = explode("\n", wordwrap(str_replace('\n', "\\n\n", $string), 70, " \n"));
// Multiline string should be exported starting with a "" and newline to
// have all lines aligned on the same column.
if (count($parts) > 1) {
return "\"\"\n\"" . implode("\"\n\"", $parts) . "\"\n";
}
// Single line strings are output on the same line.
else {
return "\"$parts[0]\"\n";
}
}
}

View File

@@ -0,0 +1,90 @@
<?php
/**
* @file
* Definition of Drupal\Component\Gettext\PoMemoryWriter.
*/
/**
* Defines a Gettext PO memory writer, to be used by the installer.
*/
class PoMemoryWriter implements PoWriterInterface {
/**
* Array to hold all PoItem elements.
*
* @var array
*/
private $_items;
/**
* Constructor, initialize empty items.
*/
function __construct() {
$this->_items = array();
}
/**
* Implements PoWriterInterface::writeItem().
*/
public function writeItem(PoItem $item) {
if (is_array($item->getSource())) {
$item->setSource(implode(L10N_UPDATE_PLURAL_DELIMITER, $item->getSource()));
$item->setTranslation(implode(L10N_UPDATE_PLURAL_DELIMITER, $item->getTranslation()));
}
$context = $item->getContext();
$this->_items[$context != NULL ? $context : ''][$item->getSource()] = $item->getTranslation();
}
/**
* Implements PoWriterInterface::writeItems().
*/
public function writeItems(PoReaderInterface $reader, $count = -1) {
$forever = $count == -1;
while (($count-- > 0 || $forever) && ($item = $reader->readItem())) {
$this->writeItem($item);
}
}
/**
* Get all stored PoItem's.
*
* @return array PoItem
*/
public function getData() {
return $this->_items;
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface:setLangcode().
*
* Not implemented. Not relevant for the MemoryWriter.
*/
function setLangcode($langcode) {
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface:getLangcode().
*
* Not implemented. Not relevant for the MemoryWriter.
*/
function getLangcode() {
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface:getHeader().
*
* Not implemented. Not relevant for the MemoryWriter.
*/
function getHeader() {
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface:setHeader().
*
* Not implemented. Not relevant for the MemoryWriter.
*/
function setHeader(PoHeader $header) {
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* @file
* Definition of Drupal\Component\Gettext\PoMetadataInterface.
*/
/**
* Methods required for both reader and writer implementations.
*
* @see Drupal\Component\Gettext\PoReaderInterface
* @see Drupal\Component\Gettext\PoWriterInterface
*/
interface PoMetadataInterface {
/**
* Set language code.
*
* @param string $langcode
* Language code string.
*/
public function setLangcode($langcode);
/**
* Get language code.
*
* @return string
* Language code string.
*/
public function getLangcode();
/**
* Set header metadata.
*
* @param PoHeader $header
* Header object representing metadata in a PO header.
*/
public function setHeader(PoHeader $header);
/**
* Get header metadata.
*
* @return PoHeader $header
* Header instance representing metadata in a PO header.
*/
public function getHeader();
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* @file
* Definition of Drupal\Component\Gettext\PoReaderInterface.
*/
/**
* Shared interface definition for all Gettext PO Readers.
*/
interface PoReaderInterface extends PoMetadataInterface {
/**
* Reads and returns a PoItem (source/translation pair).
*
* @return PoItem
* Wrapper for item data instance.
*/
public function readItem();
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* @file
* Definition of Drupal\Component\Gettext\PoStreamInterface.
*/
/**
* Common functions for file/stream based PO readers/writers.
*
* @see PoReaderInterface
* @see PoWriterInterface
*/
interface PoStreamInterface {
/**
* Open the stream. Set the URI for the stream earlier with setURI().
*/
public function open();
/**
* Close the stream.
*/
public function close();
/**
* Get the URI of the PO stream that is being read or written.
*
* @return
* URI string for this stream.
*/
public function getURI();
/**
* Set the URI of the PO stream that is going to be read or written.
*
* @param $uri
* URI string to set for this stream.
*/
public function setURI($uri);
}

View File

@@ -0,0 +1,603 @@
<?php
/**
* @file
* Contains \Drupal\Component\Gettext\PoStreamReader.
*/
/**
* Implements Gettext PO stream reader.
*
* The PO file format parsing is implemented according to the documentation at
* http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files
*/
class PoStreamReader implements PoStreamInterface, PoReaderInterface {
/**
* Source line number of the stream being parsed.
*
* @var int
*/
private $_line_number = 0;
/**
* Parser context for the stream reader state machine.
*
* Possible contexts are:
* - 'COMMENT' (#)
* - 'MSGID' (msgid)
* - 'MSGID_PLURAL' (msgid_plural)
* - 'MSGCTXT' (msgctxt)
* - 'MSGSTR' (msgstr or msgstr[])
* - 'MSGSTR_ARR' (msgstr_arg)
*
* @var string
*/
private $_context = 'COMMENT';
/**
* Current entry being read. Incomplete.
*
* @var array
*/
private $_current_item = array();
/**
* Current plural index for plural translations.
*
* @var int
*/
private $_current_plural_index = 0;
/**
* URI of the PO stream that is being read.
*
* @var string
*/
private $_uri = '';
/**
* Language code for the PO stream being read.
*
* @var string
*/
private $_langcode = NULL;
/**
* Size of the current PO stream.
*
* @var int
*/
private $_size;
/**
* File handle of the current PO stream.
*
* @var resource
*/
private $_fd;
/**
* The PO stream header.
*
* @var PoHeader
*/
private $_header;
/**
* Object wrapper for the last read source/translation pair.
*
* @var PoItem
*/
private $_last_item;
/**
* Indicator of whether the stream reading is finished.
*
* @var boolean
*/
private $_finished;
/**
* Array of translated error strings recorded on reading this stream so far.
*
* @var array
*/
private $_errors;
/**
* Implements PoMetadataInterface::getLangcode().
*/
public function getLangcode() {
return $this->_langcode;
}
/**
* Implements PoMetadataInterface::setLangcode().
*/
public function setLangcode($langcode) {
$this->_langcode = $langcode;
}
/**
* Implements PoMetadataInterface::getHeader().
*/
public function getHeader() {
return $this->_header;
}
/**
* Implements PoMetadataInterface::setHeader().
*
* Not applicable to stream reading and therefore not implemented.
*/
public function setHeader(PoHeader $header) {
}
/**
* Implements PoStreamInterface::getURI().
*/
public function getURI() {
return $this->_uri;
}
/**
* Implements PoStreamInterface::setURI().
*/
public function setURI($uri) {
$this->_uri = $uri;
}
/**
* Implements PoStreamInterface::open().
*
* Opens the stream and reads the header. The stream is ready for reading
* items after.
*
* @throws Exception
* If the URI is not yet set.
*/
public function open() {
if (!empty($this->_uri)) {
$this->_fd = fopen($this->_uri, 'rb');
$this->_size = ftell($this->_fd);
$this->readHeader();
}
else {
throw new \Exception('Cannot open stream without URI set.');
}
}
/**
* Implements PoStreamInterface::close().
*
* @throws Exception
* If the stream is not open.
*/
public function close() {
if ($this->_fd) {
fclose($this->_fd);
}
else {
throw new \Exception('Cannot close stream that is not open.');
}
}
/**
* Implements PoReaderInterface::readItem().
*/
public function readItem() {
// Clear out the last item.
$this->_last_item = NULL;
// Read until finished with the stream or a complete item was identified.
while (!$this->_finished && is_null($this->_last_item)) {
$this->readLine();
}
return $this->_last_item;
}
/**
* Sets the seek position for the current PO stream.
*
* @param int $seek
* The new seek position to set.
*/
public function setSeek($seek) {
fseek($this->_fd, $seek);
}
/**
* Returns the pointer position of the current PO stream.
*/
public function getSeek() {
return ftell($this->_fd);
}
/**
* Read the header from the PO stream.
*
* The header is a special case PoItem, using the empty string as source and
* key-value pairs as translation. We just reuse the item reader logic to
* read the header.
*/
private function readHeader() {
$item = $this->readItem();
// Handle the case properly when the .po file is empty (0 bytes).
if (!$item) {
return;
}
$header = new PoHeader;
$header->setFromString(trim($item->getTranslation()));
$this->_header = $header;
}
/**
* Reads a line from the PO stream and stores data internally.
*
* Expands $this->_current_item based on new data for the current item. If
* this line ends the current item, it is saved with setItemFromArray() with
* data from $this->_current_item.
*
* An internal state machine is maintained in this reader using $this->_context
* as the reading state. PO items are inbetween COMMENT states (when items have
* at least one line or comment inbetween them or indicated by MSGSTR or
* MSGSTR_ARR followed immediately by an MSGID or MSGCTXT (when items closely
* follow each other).
*
* @return
* FALSE if an error was logged, NULL otherwise. The errors are considered
* non-blocking, so reading can continue, while the errors are collected
* for later presentation.
*/
private function readLine() {
// Read a line and set the stream finished indicator if it was not
// possible anymore.
$line = fgets($this->_fd);
$this->_finished = ($line === FALSE);
if (!$this->_finished) {
if ($this->_line_number == 0) {
// The first line might come with a UTF-8 BOM, which should be removed.
$line = str_replace("\xEF\xBB\xBF", '', $line);
// Current plurality for 'msgstr[]'.
$this->_current_plural_index = 0;
}
// Track the line number for error reporting.
$this->_line_number++;
// Initialize common values for error logging.
$log_vars = array(
'%uri' => $this->getURI(),
'%line' => $this->_line_number,
);
// Trim away the linefeed. \\n might appear at the end of the string if
// another line continuing the same string follows. We can remove that.
$line = trim(strtr($line, array("\\\n" => "")));
if (!strncmp('#', $line, 1)) {
// Lines starting with '#' are comments.
if ($this->_context == 'COMMENT') {
// Already in comment context, add to current comment.
$this->_current_item['#'][] = substr($line, 1);
}
elseif (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) {
// We are currently in string context, save current item.
$this->setItemFromArray($this->_current_item);
// Start a new entry for the comment.
$this->_current_item = array();
$this->_current_item['#'][] = substr($line, 1);
$this->_context = 'COMMENT';
return;
}
else {
// A comment following any other context is a syntax error.
$this->_errors[] = format_string('The translation stream %uri contains an error: "msgstr" was expected but not found on line %line.', $log_vars);
return FALSE;
}
return;
}
elseif (!strncmp('msgid_plural', $line, 12)) {
// A plural form for the current source string.
if ($this->_context != 'MSGID') {
// A plural form can only be added to an msgid directly.
$this->_errors[] = format_string('The translation stream %uri contains an error: "msgid_plural" was expected but not found on line %line.', $log_vars);
return FALSE;
}
// Remove 'msgid_plural' and trim away whitespace.
$line = trim(substr($line, 12));
// Only the plural source string is left, parse it.
$quoted = $this->parseQuoted($line);
if ($quoted === FALSE) {
// The plural form must be wrapped in quotes.
$this->_errors[] = format_string('The translation stream %uri contains a syntax error on line %line.', $log_vars);
return FALSE;
}
// Append the plural source to the current entry.
if (is_string($this->_current_item['msgid'])) {
// The first value was stored as string. Now we know the context is
// plural, it is converted to array.
$this->_current_item['msgid'] = array($this->_current_item['msgid']);
}
$this->_current_item['msgid'][] = $quoted;
$this->_context = 'MSGID_PLURAL';
return;
}
elseif (!strncmp('msgid', $line, 5)) {
// Starting a new message.
if (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) {
// We are currently in string context, save current item.
$this->setItemFromArray($this->_current_item);
// Start a new context for the msgid.
$this->_current_item = array();
}
elseif ($this->_context == 'MSGID') {
// We are currently already in the context, meaning we passed an id with no data.
$this->_errors[] = format_string('The translation stream %uri contains an error: "msgid" is unexpected on line %line.', $log_vars);
return FALSE;
}
// Remove 'msgid' and trim away whitespace.
$line = trim(substr($line, 5));
// Only the message id string is left, parse it.
$quoted = $this->parseQuoted($line);
if ($quoted === FALSE) {
// The message id must be wrapped in quotes.
$this->_errors[] = format_string('The translation stream %uri contains an error: invalid format for "msgid" on line %line.', $log_vars, $log_vars);
return FALSE;
}
$this->_current_item['msgid'] = $quoted;
$this->_context = 'MSGID';
return;
}
elseif (!strncmp('msgctxt', $line, 7)) {
// Starting a new context.
if (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) {
// We are currently in string context, save current item.
$this->setItemFromArray($this->_current_item);
$this->_current_item = array();
}
elseif (!empty($this->_current_item['msgctxt'])) {
// A context cannot apply to another context.
$this->_errors[] = format_string('The translation stream %uri contains an error: "msgctxt" is unexpected on line %line.', $log_vars);
return FALSE;
}
// Remove 'msgctxt' and trim away whitespaces.
$line = trim(substr($line, 7));
// Only the msgctxt string is left, parse it.
$quoted = $this->parseQuoted($line);
if ($quoted === FALSE) {
// The context string must be quoted.
$this->_errors[] = format_string('The translation stream %uri contains an error: invalid format for "msgctxt" on line %line.', $log_vars);
return FALSE;
}
$this->_current_item['msgctxt'] = $quoted;
$this->_context = 'MSGCTXT';
return;
}
elseif (!strncmp('msgstr[', $line, 7)) {
// A message string for a specific plurality.
if (($this->_context != 'MSGID') &&
($this->_context != 'MSGCTXT') &&
($this->_context != 'MSGID_PLURAL') &&
($this->_context != 'MSGSTR_ARR')) {
// Plural message strings must come after msgid, msgxtxt,
// msgid_plural, or other msgstr[] entries.
$this->_errors[] = format_string('The translation stream %uri contains an error: "msgstr[]" is unexpected on line %line.', $log_vars);
return FALSE;
}
// Ensure the plurality is terminated.
if (strpos($line, ']') === FALSE) {
$this->_errors[] = format_string('The translation stream %uri contains an error: invalid format for "msgstr[]" on line %line.', $log_vars);
return FALSE;
}
// Extract the plurality.
$frombracket = strstr($line, '[');
$this->_current_plural_index = substr($frombracket, 1, strpos($frombracket, ']') - 1);
// Skip to the next whitespace and trim away any further whitespace,
// bringing $line to the message text only.
$line = trim(strstr($line, " "));
$quoted = $this->parseQuoted($line);
if ($quoted === FALSE) {
// The string must be quoted.
$this->_errors[] = format_string('The translation stream %uri contains an error: invalid format for "msgstr[]" on line %line.', $log_vars);
return FALSE;
}
if (!isset($this->_current_item['msgstr']) || !is_array($this->_current_item['msgstr'])) {
$this->_current_item['msgstr'] = array();
}
$this->_current_item['msgstr'][$this->_current_plural_index] = $quoted;
$this->_context = 'MSGSTR_ARR';
return;
}
elseif (!strncmp("msgstr", $line, 6)) {
// A string pair for an msgidid (with optional context).
if (($this->_context != 'MSGID') && ($this->_context != 'MSGCTXT')) {
// Strings are only valid within an id or context scope.
$this->_errors[] = format_string('The translation stream %uri contains an error: "msgstr" is unexpected on line %line.', $log_vars);
return FALSE;
}
// Remove 'msgstr' and trim away away whitespaces.
$line = trim(substr($line, 6));
// Only the msgstr string is left, parse it.
$quoted = $this->parseQuoted($line);
if ($quoted === FALSE) {
// The string must be quoted.
$this->_errors[] = format_string('The translation stream %uri contains an error: invalid format for "msgstr" on line %line.', $log_vars);
return FALSE;
}
$this->_current_item['msgstr'] = $quoted;
$this->_context = 'MSGSTR';
return;
}
elseif ($line != '') {
// Anything that is not a token may be a continuation of a previous token.
$quoted = $this->parseQuoted($line);
if ($quoted === FALSE) {
// This string must be quoted.
$this->_errors[] = format_string('The translation stream %uri contains an error: string continuation expected on line %line.', $log_vars);
return FALSE;
}
// Append the string to the current item.
if (($this->_context == 'MSGID') || ($this->_context == 'MSGID_PLURAL')) {
if (is_array($this->_current_item['msgid'])) {
// Add string to last array element for plural sources.
$last_index = count($this->_current_item['msgid']) - 1;
$this->_current_item['msgid'][$last_index] .= $quoted;
}
else {
// Singular source, just append the string.
$this->_current_item['msgid'] .= $quoted;
}
}
elseif ($this->_context == 'MSGCTXT') {
// Multiline context name.
$this->_current_item['msgctxt'] .= $quoted;
}
elseif ($this->_context == 'MSGSTR') {
// Multiline translation string.
$this->_current_item['msgstr'] .= $quoted;
}
elseif ($this->_context == 'MSGSTR_ARR') {
// Multiline plural translation string.
$this->_current_item['msgstr'][$this->_current_plural_index] .= $quoted;
}
else {
// No valid context to append to.
$this->_errors[] = format_string('The translation stream %uri contains an error: unexpected string on line %line.', $log_vars);
return FALSE;
}
return;
}
}
// Empty line read or EOF of PO stream, close out the last entry.
if (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) {
$this->setItemFromArray($this->_current_item);
$this->_current_item = array();
}
elseif ($this->_context != 'COMMENT') {
$this->_errors[] = format_string('The translation stream %uri ended unexpectedly at line %line.', $log_vars);
return FALSE;
}
}
/**
* Store the parsed values as a PoItem object.
*/
public function setItemFromArray($value) {
$plural = FALSE;
$comments = '';
if (isset($value['#'])) {
$comments = $this->shortenComments($value['#']);
}
if (is_array($value['msgstr'])) {
// Sort plural variants by their form index.
ksort($value['msgstr']);
$plural = TRUE;
}
$item = new PoItem();
$item->setContext(isset($value['msgctxt']) ? $value['msgctxt'] : '');
$item->setSource($value['msgid']);
$item->setTranslation($value['msgstr']);
$item->setPlural($plural);
$item->setComment($comments);
$item->setLangcode($this->_langcode);
$this->_last_item = $item;
$this->_context = 'COMMENT';
}
/**
* Parses a string in quotes.
*
* @param $string
* A string specified with enclosing quotes.
*
* @return
* The string parsed from inside the quotes.
*/
function parseQuoted($string) {
if (substr($string, 0, 1) != substr($string, -1, 1)) {
// Start and end quotes must be the same.
return FALSE;
}
$quote = substr($string, 0, 1);
$string = substr($string, 1, -1);
if ($quote == '"') {
// Double quotes: strip slashes.
return stripcslashes($string);
}
elseif ($quote == "'") {
// Simple quote: return as-is.
return $string;
}
else {
// Unrecognized quote.
return FALSE;
}
}
/**
* Generates a short, one-string version of the passed comment array.
*
* @param $comment
* An array of strings containing a comment.
*
* @return
* Short one-string version of the comment.
*/
private function shortenComments($comment) {
$comm = '';
while (count($comment)) {
$test = $comm . substr(array_shift($comment), 1) . ', ';
if (strlen($comm) < 130) {
$comm = $test;
}
else {
break;
}
}
return trim(substr($comm, 0, -2));
}
}

View File

@@ -0,0 +1,160 @@
<?php
/**
* @file
* Definition of Drupal\Component\Gettext\PoStreamWriter.
*/
/**
* Defines a Gettext PO stream writer.
*/
class PoStreamWriter implements PoWriterInterface, PoStreamInterface {
/**
* URI of the PO stream that is being written.
*
* @var string
*/
private $_uri;
/**
* The Gettext PO header.
*
* @var PoHeader
*/
private $_header;
/**
* File handle of the current PO stream.
*
* @var resource
*/
private $_fd;
/**
* Get the PO header of the current stream.
*
* @return PoHeader
* The Gettext PO header.
*/
public function getHeader() {
return $this->_header;
}
/**
* Set the PO header for the current stream.
*
* @param PoHeader $header
* The Gettext PO header to set.
*/
public function setHeader(PoHeader $header) {
$this->_header = $header;
}
/**
* Get the current language code used.
*
* @return string
* The language code.
*/
public function getLangcode() {
return $this->_langcode;
}
/**
* Set the language code.
*
* @param string $langcode
* The language code.
*/
public function setLangcode($langcode) {
$this->_langcode = $langcode;
}
/**
* Implements PoStreamInterface::open().
*/
public function open() {
// Open in write mode. Will overwrite the stream if it already exists.
$this->_fd = fopen($this->getURI(), 'w');
// Write the header at the start.
$this->writeHeader();
}
/**
* Implements PoStreamInterface::close().
*
* @throws Exception
* If the stream is not open.
*/
public function close() {
if ($this->_fd) {
fclose($this->_fd);
}
else {
throw new Exception('Cannot close stream that is not open.');
}
}
/**
* Write data to the stream.
*
* @param string $data
* Piece of string to write to the stream. If the value is not directly a
* string, casting will happen in writing.
*
* @throws Exception
* If writing the data is not possible.
*/
private function write($data) {
$result = fputs($this->_fd, $data);
if ($result === FALSE) {
throw new Exception('Unable to write data: ' . substr($data, 0, 20));
}
}
/**
* Write the PO header to the stream.
*/
private function writeHeader() {
$this->write($this->_header);
}
/**
* Implements PoWriterInterface::writeItem().
*/
public function writeItem(PoItem $item) {
$this->write($item);
}
/**
* Implements PoWriterInterface::writeItems().
*/
public function writeItems(PoReaderInterface $reader, $count = -1) {
$forever = $count == -1;
while (($count-- > 0 || $forever) && ($item = $reader->readItem())) {
$this->writeItem($item);
}
}
/**
* Implements PoStreamInterface::getURI().
*
* @throws Exception
* If the URI is not set.
*/
public function getURI() {
if (empty($this->_uri)) {
throw new Exception('No URI set.');
}
return $this->_uri;
}
/**
* Implements PoStreamInterface::setURI().
*/
public function setURI($uri) {
$this->_uri = $uri;
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* @file
* Definition of Drupal\Component\Gettext\PoWriterInterface.
*/
/**
* Shared interface definition for all Gettext PO Writers.
*/
interface PoWriterInterface extends PoMetadataInterface {
/**
* Writes the given item.
*
* @param PoItem $item
* One specific item to write.
*/
public function writeItem(PoItem $item);
/**
* Writes all or the given amount of items.
*
* @param PoReaderInterface $reader
* Reader to read PoItems from.
* @param $count
* Amount of items to read from $reader to write. If -1, all items are
* read from $reader.
*/
public function writeItems(PoReaderInterface $reader, $count = -1);
}

View File

@@ -0,0 +1,97 @@
<?php
/**
* @file
* Definition of Gettext class.
*/
/**
* Static class providing Drupal specific Gettext functionality.
*
* The operations are related to pumping data from a source to a destination,
* for example:
* - Remote files http://*.po to memory
* - File public://*.po to database
*/
class Gettext {
/**
* Reads the given PO files into the database.
*
* @param stdClass $file
* File object with an URI property pointing at the file's path.
* - "langcode": The language the strings will be added to.
* - "uri": File URI.
* @param array $options
* An array with options that can have the following elements:
* - 'overwrite_options': Overwrite options array as defined in
* PoDatabaseWriter. Optional, defaults to an empty array.
* - 'customized': Flag indicating whether the strings imported from $file
* are customized translations or come from a community source. Use
* L10N_UPDATE_CUSTOMIZED or L10N_UPDATE_NOT_CUSTOMIZED. Optional, defaults to
* L10N_UPDATE_NOT_CUSTOMIZED.
* - 'seek': Specifies from which position in the file should the reader
* start reading the next items. Optional, defaults to 0.
* - 'items': Specifies the number of items to read. Optional, defaults to
* -1, which means that all the items from the stream will be read.
*
* @return array
* Report array as defined in PoDatabaseWriter.
*
* @see PoDatabaseWriter
*/
static function fileToDatabase($file, $options) {
// Add the default values to the options array.
$options += array(
'overwrite_options' => array(),
'customized' => L10N_UPDATE_NOT_CUSTOMIZED,
'items' => -1,
'seek' => 0,
);
// Instantiate and initialize the stream reader for this file.
$reader = new PoStreamReader();
$reader->setLangcode($file->langcode);
$reader->setURI($file->uri);
try {
$reader->open();
}
catch (\Exception $exception) {
throw $exception;
}
$header = $reader->getHeader();
if (!$header) {
throw new \Exception('Missing or malformed header.');
}
// Initialize the database writer.
$writer = new PoDatabaseWriter();
$writer->setLangcode($file->langcode);
$writer_options = array(
'overwrite_options' => $options['overwrite_options'],
'customized' => $options['customized'],
);
$writer->setOptions($writer_options);
$writer->setHeader($header);
// Attempt to pipe all items from the file to the database.
try {
if ($options['seek']) {
$reader->setSeek($options['seek']);
}
$writer->writeItems($reader, $options['items']);
}
catch (\Exception $exception) {
throw $exception;
}
// Report back with an array of status information.
$report = $writer->getReport();
// Add the seek position to the report. This is useful for the batch
// operation.
$report['seek'] = $reader->getSeek();
return $report;
}
}

View File

@@ -0,0 +1,178 @@
<?php
/**
* @file
* Definition of PoDatabaseReader.
*/
/**
* Gettext PO reader working with the locale module database.
*/
class PoDatabaseReader implements PoReaderInterface {
/**
* An associative array indicating which type of strings should be read.
*
* Elements of the array:
* - not_customized: boolean indicating if not customized strings should be
* read.
* - customized: boolean indicating if customized strings should be read.
* - no_translated: boolean indicating if non-translated should be read.
*
* The three options define three distinct sets of strings, which combined
* cover all strings.
*
* @var array
*/
private $_options;
/**
* Language code of the language being read from the database.
*
* @var string
*/
private $_langcode;
/**
* Store the result of the query so it can be iterated later.
*
* @var resource
*/
private $_result;
/**
* Database storage to retrieve the strings from.
*
* @var StringDatabaseStorage
*/
protected $storage;
/**
* Constructor, initializes with default options.
*/
function __construct() {
$this->setOptions(array());
$this->storage = new StringDatabaseStorage();
}
/**
* Implements PoMetadataInterface::getLangcode().
*/
public function getLangcode() {
return $this->_langcode;
}
/**
* Implements PoMetadataInterface::setLangcode().
*/
public function setLangcode($langcode) {
$this->_langcode = $langcode;
}
/**
* Get the options used by the reader.
*/
function getOptions() {
return $this->_options;
}
/**
* Set the options for the current reader.
*/
function setOptions(array $options) {
$options += array(
'customized' => FALSE,
'not_customized' => FALSE,
'not_translated' => FALSE,
);
$this->_options = $options;
}
/**
* Implements PoMetadataInterface::getHeader().
*/
function getHeader() {
return new PoHeader($this->getLangcode());
}
/**
* Implements PoMetadataInterface::setHeader().
*
* @throws Exception
* Always, because you cannot set the PO header of a reader.
*/
function setHeader(PoHeader $header) {
throw new \Exception('You cannot set the PO header in a reader.');
}
/**
* Builds and executes a database query based on options set earlier.
*/
private function loadStrings() {
$langcode = $this->_langcode;
$options = $this->_options;
$conditions = array();
if (array_sum($options) == 0) {
// If user asked to not include anything in the translation files,
// that would not make sense, so just fall back on providing a template.
$langcode = NULL;
// Force option to get both translated and untranslated strings.
$options['not_translated'] = TRUE;
}
// Build and execute query to collect source strings and translations.
if (!empty($langcode)) {
$conditions['language'] = $langcode;
// Translate some options into field conditions.
if ($options['customized']) {
if (!$options['not_customized']) {
// Filter for customized strings only.
$conditions['customized'] = L10N_UPDATE_CUSTOMIZED;
}
// Else no filtering needed in this case.
}
else {
if ($options['not_customized']) {
// Filter for non-customized strings only.
$conditions['customized'] = L10N_UPDATE_NOT_CUSTOMIZED;
}
else {
// Filter for strings without translation.
$conditions['translated'] = FALSE;
}
}
if (!$options['not_translated']) {
// Filter for string with translation.
$conditions['translated'] = TRUE;
}
return $this->storage->getTranslations($conditions);
}
else {
// If no language, we don't need any of the target fields.
return $this->storage->getStrings($conditions);
}
}
/**
* Get the database result resource for the given language and options.
*/
private function readString() {
if (!isset($this->_result)) {
$this->_result = $this->loadStrings();
}
return array_shift($this->_result);
}
/**
* Implements PoReaderInterface::readItem().
*/
function readItem() {
if ($string = $this->readString()) {
$values = (array)$string;
$poItem = new PoItem();
$poItem->setFromArray($values);
return $poItem;
}
}
}

View File

@@ -0,0 +1,296 @@
<?php
/**
* @file
* Definition of PoDatabaseWriter.
*/
/**
* Gettext PO writer working with the locale module database.
*/
class PoDatabaseWriter implements PoWriterInterface {
/**
* An associative array indicating what data should be overwritten, if any.
*
* Elements of the array:
* - override_options
* - not_customized: boolean indicating that not customized strings should
* be overwritten.
* - customized: boolean indicating that customized strings should be
* overwritten.
* - customized: the strings being imported should be saved as customized.
* One of L10N_UPDATE_CUSTOMIZED or L10N_UPDATE_NOT_CUSTOMIZED.
*
* @var array
*/
private $_options;
/**
* Language code of the language being written to the database.
*
* @var string
*/
private $_langcode;
/**
* Header of the po file written to the database.
*
* @var PoHeader
*/
private $_header;
/**
* Associative array summarizing the number of changes done.
*
* Keys for the array:
* - additions: number of source strings newly added
* - updates: number of translations updated
* - deletes: number of translations deleted
* - skips: number of strings skipped due to disallowed HTML
*
* @var array
*/
private $_report;
/**
* Database storage to store the strings in.
*
* @var StringDatabaseStorage
*/
protected $storage;
/**
* Constructor, initialize reporting array.
*/
function __construct() {
$this->setReport();
$this->storage = new StringDatabaseStorage();
}
/**
* Implements PoMetadataInterface::getLangcode().
*/
public function getLangcode() {
return $this->_langcode;
}
/**
* Implements PoMetadataInterface::setLangcode().
*/
public function setLangcode($langcode) {
$this->_langcode = $langcode;
}
/**
* Get the report of the write operations.
*/
public function getReport() {
return $this->_report;
}
/**
* Set the report array of write operations.
*
* @param array $report
* Associative array with result information.
*/
function setReport($report = array()) {
$report += array(
'additions' => 0,
'updates' => 0,
'deletes' => 0,
'skips' => 0,
'strings' => array(),
);
$this->_report = $report;
}
/**
* Get the options used by the writer.
*/
function getOptions() {
return $this->_options;
}
/**
* Set the options for the current writer.
*/
function setOptions(array $options) {
if (!isset($options['overwrite_options'])) {
$options['overwrite_options'] = array();
}
$options['overwrite_options'] += array(
'not_customized' => FALSE,
'customized' => FALSE,
);
$options += array(
'customized' => L10N_UPDATE_NOT_CUSTOMIZED,
);
$this->_options = $options;
}
/**
* Implements PoMetadataInterface::getHeader().
*/
function getHeader() {
return $this->_header;
}
/**
* Implements PoMetadataInterface::setHeader().
*
* Sets the header and configure Drupal accordingly.
*
* Before being able to process the given header we need to know in what
* context this database write is done. For this the options must be set.
*
* A langcode is required to set the current header's PluralForm.
*
* @param PoHeader $header
* Header metadata.
*
* @throws Exception
*/
function setHeader(PoHeader $header) {
$this->_header = $header;
$languages = language_list();
// Check for options.
$options = $this->getOptions();
if (empty($options)) {
throw new \Exception('Options should be set before assigning a PoHeader.');
}
$overwrite_options = $options['overwrite_options'];
// Check for langcode.
$langcode = $this->_langcode;
if (empty($langcode)) {
throw new \Exception('Langcode should be set before assigning a PoHeader.');
}
// Check is language is already created.
if (!isset($languages[$langcode])) {
throw new \Exception('Language should be known before using it.');
}
if (array_sum($overwrite_options) || empty($languages[$langcode]->plurals)) {
// Get and store the plural formula if available.
$plural = $header->getPluralForms();
if (isset($plural) && $p = $header->parsePluralForms($plural)) {
list($nplurals, $formula) = $p;
db_update('languages')
->fields(array(
'plurals' => $nplurals,
'formula' => $formula,
))
->condition('language', $langcode)
->execute();
}
}
}
/**
* Implements PoWriterInterface::writeItem().
*/
function writeItem(PoItem $item) {
if ($item->isPlural()) {
$item->setSource(join(L10N_UPDATE_PLURAL_DELIMITER, $item->getSource()));
$item->setTranslation(join(L10N_UPDATE_PLURAL_DELIMITER, $item->getTranslation()));
}
$this->importString($item);
}
/**
* Implements PoWriterInterface::writeItems().
*/
public function writeItems(PoReaderInterface $reader, $count = -1) {
$forever = $count == -1;
while (($count-- > 0 || $forever) && ($item = $reader->readItem())) {
$this->writeItem($item);
}
}
/**
* Imports one string into the database.
*
* @param PoItem $item
* The item being imported.
*
* @return int
* The string ID of the existing string modified or the new string added.
*/
private function importString(PoItem $item) {
// Initialize overwrite options if not set.
$this->_options['overwrite_options'] += array(
'not_customized' => FALSE,
'customized' => FALSE,
);
$overwrite_options = $this->_options['overwrite_options'];
$customized = $this->_options['customized'];
$context = $item->getContext();
$source = $item->getSource();
$translation = $item->getTranslation();
// Look up the source string and any existing translation.
$strings = $this->storage->getTranslations(array(
'language' => $this->_langcode,
'source' => $source,
'context' => $context
));
$string = reset($strings);
if (!empty($translation)) {
// Skip this string unless it passes a check for dangerous code.
if (!locale_string_is_safe($translation)) {
watchdog('l10n_update', 'Import of string "%string" was skipped because of disallowed or malformed HTML.', array('%string' => $translation), WATCHDOG_ERROR);
$this->_report['skips']++;
return 0;
}
elseif ($string) {
$string->setString($translation);
if ($string->isNew()) {
// No translation in this language.
$string->setValues(array(
'language' => $this->_langcode,
'customized' => $customized
));
$string->save();
$this->_report['additions']++;
}
elseif ($overwrite_options[$string->customized ? 'customized' : 'not_customized']) {
// Translation exists, only overwrite if instructed.
$string->customized = $customized;
$string->save();
$this->_report['updates']++;
}
$this->_report['strings'][] = $string->getId();
return $string->lid;
}
else {
// No such source string in the database yet.
$string = $this->storage->createString(array('source' => $source, 'context' => $context))
->save();
$target = $this->storage->createTranslation(array(
'lid' => $string->getId(),
'language' => $this->_langcode,
'translation' => $translation,
'customized' => $customized,
))->save();
$this->_report['additions']++;
$this->_report['strings'][] = $string->getId();
return $string->lid;
}
}
elseif ($string && !$string->isNew() && $overwrite_options[$string->customized ? 'customized' : 'not_customized']) {
// Empty translation, remove existing if instructed.
$string->delete();
$this->_report['deletes']++;
$this->_report['strings'][] = $string->lid;
return $string->lid;
}
}
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* @file
* Definition of SourceString.
*/
/**
* Defines the locale source string object.
*
* This class represents a module-defined string value that is to be translated.
* This string must at least contain a 'source' field, which is the raw source
* value, and is assumed to be in English language.
*/
class SourceString extends StringBase {
/**
* Implements StringInterface::isSource().
*/
public function isSource() {
return isset($this->source);
}
/**
* Implements StringInterface::isTranslation().
*/
public function isTranslation() {
return FALSE;
}
/**
* Implements LocaleString::getString().
*/
public function getString() {
return isset($this->source) ? $this->source : '';
}
/**
* Implements LocaleString::setString().
*/
public function setString($string) {
$this->source = $string;
return $this;
}
/**
* Implements LocaleString::isNew().
*/
public function isNew() {
return empty($this->lid);
}
}

View File

@@ -0,0 +1,184 @@
<?php
/**
* @file
* Definition of StringBase.
*/
/**
* Defines the locale string base class.
*
* This is the base class to be used for locale string objects and contains
* the common properties and methods for source and translation strings.
*/
abstract class StringBase implements StringInterface {
/**
* The string identifier.
*
* @var integer
*/
public $lid;
/**
* The string locations indexed by type.
*
* @var string
*/
public $locations;
/**
* The source string.
*
* @var string
*/
public $source;
/**
* The string context.
*
* @var string
*/
public $context;
/**
* The string version.
*
* @var string
*/
public $version;
/**
* The locale storage this string comes from or is to be saved to.
*
* @var StringStorageInterface
*/
protected $storage;
/**
* Constructs a new locale string object.
*
* @param object|array $values
* Object or array with initial values.
*/
public function __construct($values = array()) {
$this->setValues((array)$values);
}
/**
* Implements StringInterface::getId().
*/
public function getId() {
return isset($this->lid) ? $this->lid : NULL;
}
/**
* Implements StringInterface::setId().
*/
public function setId($lid) {
$this->lid = $lid;
return $this;
}
/**
* Implements StringInterface::getVersion().
*/
public function getVersion() {
return isset($this->version) ? $this->version : NULL;
}
/**
* Implements StringInterface::setVersion().
*/
public function setVersion($version) {
$this->version = $version;
return $this;
}
/**
* Implements StringInterface::getPlurals().
*/
public function getPlurals() {
return explode(L10N_UPDATE_PLURAL_DELIMITER, $this->getString());
}
/**
* Implements StringInterface::setPlurals().
*/
public function setPlurals($plurals) {
$this->setString(implode(L10N_UPDATE_PLURAL_DELIMITER, $plurals));
return $this;
}
/**
* Implements StringInterface::getStorage().
*/
public function getStorage() {
return isset($this->storage) ? $this->storage : NULL;
}
/**
* Implements StringInterface::setStorage().
*/
public function setStorage($storage) {
$this->storage = $storage;
return $this;
}
/**
* Implements StringInterface::setValues().
*/
public function setValues(array $values, $override = TRUE) {
foreach ($values as $key => $value) {
if (property_exists($this, $key) && ($override || !isset($this->$key))) {
$this->$key = $value;
}
}
return $this;
}
/**
* Implements StringInterface::getValues().
*/
public function getValues(array $fields) {
$values = array();
foreach ($fields as $field) {
if (isset($this->$field)) {
$values[$field] = $this->$field;
}
}
return $values;
}
/**
* Implements LocaleString::save().
*/
public function save() {
if ($storage = $this->getStorage()) {
$storage->save($this);
}
else {
throw new StringStorageException(format_string('The string cannot be saved because its not bound to a storage: @string', array(
'@string' => $this->getString()
)));
}
return $this;
}
/**
* Implements LocaleString::delete().
*/
public function delete() {
if (!$this->isNew()) {
if ($storage = $this->getStorage()) {
$storage->delete($this);
}
else {
throw new StringStorageException(format_string('The string cannot be deleted because its not bound to a storage: @string', array(
'@string' => $this->getString()
)));
}
}
return $this;
}
}

View File

@@ -0,0 +1,518 @@
<?php
/**
* @file
* Definition of StringDatabaseStorage.
*/
/**
* Defines the locale string class.
*
* This is the base class for SourceString and TranslationString.
*/
class StringDatabaseStorage implements StringStorageInterface {
/**
* Additional database connection options to use in queries.
*
* @var array
*/
protected $options = array();
/**
* Constructs a new StringStorage controller.
*
* @param array $options
* (optional) Any additional database connection options to use in queries.
*/
public function __construct(array $options = array()) {
$this->options = $options;
}
/**
* Implements StringStorageInterface::getStrings().
*/
public function getStrings(array $conditions = array(), array $options = array()) {
return $this->dbStringLoad($conditions, $options, 'SourceString');
}
/**
* Implements StringStorageInterface::getTranslations().
*/
public function getTranslations(array $conditions = array(), array $options = array()) {
return $this->dbStringLoad($conditions, array('translation' => TRUE) + $options, 'TranslationString');
}
/**
* Implements StringStorageInterface::findString().
*/
public function findString(array $conditions) {
$values = $this->dbStringSelect($conditions)
->execute()
->fetchAssoc();
if (!empty($values)) {
$string = new SourceString($values);
$string->setStorage($this);
return $string;
}
}
/**
* Implements StringStorageInterface::findTranslation().
*/
public function findTranslation(array $conditions) {
$values = $this->dbStringSelect($conditions, array('translation' => TRUE))
->execute()
->fetchAssoc();
if (!empty($values)) {
$string = new TranslationString($values);
$this->checkVersion($string, VERSION);
$string->setStorage($this);
return $string;
}
}
/**
* Implements StringStorageInterface::countStrings().
*/
public function countStrings() {
return $this->dbExecute("SELECT COUNT(*) FROM {locales_source}")->fetchField();
}
/**
* Implements StringStorageInterface::countTranslations().
*/
public function countTranslations() {
return $this->dbExecute("SELECT t.language, COUNT(*) AS translated FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid GROUP BY t.language")->fetchAllKeyed();
}
/**
* Implements StringStorageInterface::save().
*/
public function save($string) {
if ($string->isNew()) {
$result = $this->dbStringInsert($string);
if ($string->isSource() && $result) {
// Only for source strings, we set the locale identifier.
$string->setId($result);
}
$string->setStorage($this);
}
else {
$this->dbStringUpdate($string);
}
return $this;
}
/**
* Checks whether the string version matches a given version, fix it if not.
*
* @param StringInterface $string
* The string object.
* @param string $version
* Drupal version to check against.
*/
protected function checkVersion($string, $version) {
if ($string->getId() && $string->getVersion() != $version) {
$string->setVersion($version);
db_update('locales_source', $this->options)
->condition('lid', $string->getId())
->fields(array('version' => $version))
->execute();
}
}
/**
* Implements StringStorageInterface::delete().
*/
public function delete($string) {
if ($keys = $this->dbStringKeys($string)) {
$this->dbDelete('locales_target', $keys)->execute();
if ($string->isSource()) {
$this->dbDelete('locales_source', $keys)->execute();
$this->dbDelete('locales_location', $keys)->execute();
$string->setId(NULL);
}
}
else {
throw new StringStorageException(format_string('The string cannot be deleted because it lacks some key fields: @string', array(
'@string' => $string->getString()
)));
}
return $this;
}
/**
* Implements StringStorageInterface::deleteLanguage().
*/
public function deleteStrings($conditions) {
$lids = $this->dbStringSelect($conditions, array('fields' => array('lid')))->execute()->fetchCol();
if ($lids) {
$this->dbDelete('locales_target', array('lid' => $lids))->execute();
$this->dbDelete('locales_source', array('lid' => $lids))->execute();
$this->dbDelete('locales_location', array('sid' => $lids))->execute();
}
}
/**
* Implements StringStorageInterface::deleteLanguage().
*/
public function deleteTranslations($conditions) {
$this->dbDelete('locales_target', $conditions)->execute();
}
/**
* Implements StringStorageInterface::createString().
*/
public function createString($values = array()) {
return new SourceString($values + array('storage' => $this));
}
/**
* Implements StringStorageInterface::createTranslation().
*/
public function createTranslation($values = array()) {
return new TranslationString($values + array(
'storage' => $this,
'is_new' => TRUE
));
}
/**
* Gets table alias for field.
*
* @param string $field
* Field name to find the table alias for.
*
* @return string
* Either 's', 't' or 'l' depending on whether the field belongs to source,
* target or location table table.
*/
protected function dbFieldTable($field) {
if (in_array($field, array('language', 'translation', 'customized'))) {
return 't';
}
elseif (in_array($field, array('type', 'name'))) {
return 'l';
}
else {
return 's';
}
}
/**
* Gets table name for storing string object.
*
* @param StringInterface $string
* The string object.
*
* @return string
* The table name.
*/
protected function dbStringTable($string) {
if ($string->isSource()) {
return 'locales_source';
}
elseif ($string->isTranslation()) {
return 'locales_target';
}
}
/**
* Gets keys values that are in a database table.
*
* @param StringInterface $string
* The string object.
*
* @return array
* Array with key fields if the string has all keys, or empty array if not.
*/
protected function dbStringKeys($string) {
if ($string->isSource()) {
$keys = array('lid');
}
elseif ($string->isTranslation()) {
$keys = array('lid', 'language');
}
if (!empty($keys) && ($values = $string->getValues($keys)) && count($keys) == count($values)) {
return $values;
}
else {
return array();
}
}
/**
* Loads multiple string objects.
*
* @param array $conditions
* Any of the conditions used by dbStringSelect().
* @param array $options
* Any of the options used by dbStringSelect().
* @param string $class
* Class name to use for fetching returned objects.
*
* @return array
* Array of objects of the class requested.
*/
protected function dbStringLoad(array $conditions, array $options, $class) {
$strings = array();
$result = $this->dbStringSelect($conditions, $options)->execute();
foreach ($result as $item) {
$string = new $class($item);
$string->setStorage($this);
$strings[] = $string;
}
return $strings;
}
/**
* Builds a SELECT query with multiple conditions and fields.
*
* The query uses both 'locales_source' and 'locales_target' tables.
* Note that by default, as we are selecting both translated and untranslated
* strings target field's conditions will be modified to match NULL rows too.
*
* @param array $conditions
* An associative array with field => value conditions that may include
* NULL values. If a language condition is included it will be used for
* joining the 'locales_target' table.
* @param array $options
* An associative array of additional options. It may contain any of the
* options used by StringStorageInterface::getStrings() and these additional
* ones:
* - 'translation', Whether to include translation fields too. Defaults to
* FALSE.
* @return SelectQuery
* Query object with all the tables, fields and conditions.
*/
protected function dbStringSelect(array $conditions, array $options = array()) {
// Change field 'customized' into 'l10n_status'. This enables the Drupal 8
// backported code to work with the Drupal 7 style database tables.
if (isset($conditions['customized'])) {
$conditions['l10n_status'] = $conditions['customized'];
unset($conditions['customized']);
}
if (isset($options['customized'])) {
$options['l10n_status'] = $options['customized'];
unset($options['customized']);
}
// Start building the query with source table and check whether we need to
// join the target table too.
$query = db_select('locales_source', 's', $this->options)
->fields('s');
// Figure out how to join and translate some options into conditions.
if (isset($conditions['translated'])) {
// This is a meta-condition we need to translate into simple ones.
if ($conditions['translated']) {
// Select only translated strings.
$join = 'innerJoin';
}
else {
// Select only untranslated strings.
$join = 'leftJoin';
$conditions['translation'] = NULL;
}
unset($conditions['translated']);
}
else {
$join = !empty($options['translation']) ? 'leftJoin' : FALSE;
}
if ($join) {
if (isset($conditions['language'])) {
// If we've got a language condition, we use it for the join.
$query->$join('locales_target', 't', "t.lid = s.lid AND t.language = :langcode", array(
':langcode' => $conditions['language']
));
unset($conditions['language']);
}
else {
// Since we don't have a language, join with locale id only.
$query->$join('locales_target', 't', "t.lid = s.lid");
}
if (!empty($options['translation'])) {
// We cannot just add all fields because 'lid' may get null values.
$query->addField('t', 'language');
$query->addField('t', 'translation');
$query->addField('t', 'l10n_status', 'customized');
}
}
// If we have conditions for location's type or name, then we need the
// location table, for which we add a subquery.
if (isset($conditions['type']) || isset($conditions['name'])) {
$subquery = db_select('locales_location', 'l', $this->options)
->fields('l', array('sid'));
foreach (array('type', 'name') as $field) {
if (isset($conditions[$field])) {
$subquery->condition('l.' . $field, $conditions[$field]);
unset($conditions[$field]);
}
}
$query->condition('s.lid', $subquery, 'IN');
}
// Add conditions for both tables.
foreach ($conditions as $field => $value) {
$table_alias = $this->dbFieldTable($field);
$field_alias = $table_alias . '.' . $field;
if (is_null($value)) {
$query->isNull($field_alias);
}
elseif ($table_alias == 't' && $join === 'leftJoin') {
// Conditions for target fields when doing an outer join only make
// sense if we add also OR field IS NULL.
$query->condition(db_or()
->condition($field_alias, $value)
->isNull($field_alias)
);
}
else {
$query->condition($field_alias, $value);
}
}
// Process other options, string filter, query limit, etc...
if (!empty($options['filters'])) {
if (count($options['filters']) > 1) {
$filter = db_or();
$query->condition($filter);
}
else {
// If we have a single filter, just add it to the query.
$filter = $query;
}
foreach ($options['filters'] as $field => $string) {
$filter->condition($this->dbFieldTable($field) . '.' . $field, '%' . db_like($string) . '%', 'LIKE');
}
}
if (!empty($options['pager limit'])) {
$query = $query->extend('PagerDefault')->limit($options['pager limit']);
}
return $query;
}
/**
* Createds a database record for a string object.
*
* @param StringInterface $string
* The string object.
*
* @return bool|int
* If the operation failed, returns FALSE.
* If it succeeded returns the last insert ID of the query, if one exists.
*
* @throws StringStorageException
* If the string is not suitable for this storage, an exception ithrown.
*/
protected function dbStringInsert($string) {
if ($string->isSource()) {
$string->setValues(array('context' => '', 'version' => 'none'), FALSE);
$fields = $string->getValues(array('source', 'context', 'version'));
}
elseif ($string->isTranslation()) {
$string->setValues(array('customized' => 0), FALSE);
$fields = $string->getValues(array('lid', 'language', 'translation', 'customized'));
}
if (!empty($fields)) {
// Change field 'customized' into 'l10n_status'. This enables the Drupal 8
// backported code to work with the Drupal 7 style database tables.
if (isset($fields['customized'])) {
$fields['l10n_status'] = $fields['customized'];
unset($fields['customized']);
}
return db_insert($this->dbStringTable($string), $this->options)
->fields($fields)
->execute();
}
else {
throw new StringStorageException(format_string('The string cannot be saved: @string', array(
'@string' => $string->getString()
)));
}
}
/**
* Updates string object in the database.
*
* @param StringInterface $string
* The string object.
*
* @return bool|int
* If the record update failed, returns FALSE. If it succeeded, returns
* SAVED_NEW or SAVED_UPDATED.
*
* @throws StringStorageException
* If the string is not suitable for this storage, an exception is thrown.
*/
protected function dbStringUpdate($string) {
if ($string->isSource()) {
$values = $string->getValues(array('source', 'context', 'version'));
}
elseif ($string->isTranslation()) {
$values = $string->getValues(array('translation', 'customized'));
}
if (!empty($values) && $keys = $this->dbStringKeys($string)) {
// Change field 'customized' into 'l10n_status'. This enables the Drupal 8
// backported code to work with the Drupal 7 style database tables.
if (isset($keys['customized'])) {
$keys['l10n_status'] = $keys['customized'];
unset($keys['customized']);
}
if (isset($values['customized'])) {
$values['l10n_status'] = $values['customized'];
unset($values['customized']);
}
return db_merge($this->dbStringTable($string), $this->options)
->key($keys)
->fields($values)
->execute();
}
else {
throw new StringStorageException(format_string('The string cannot be updated: @string', array(
'@string' => $string->getString()
)));
}
}
/**
* Creates delete query.
*
* @param string $table
* The table name.
* @param array $keys
* Array with object keys indexed by field name.
*
* @return DeleteQuery
* Returns a new DeleteQuery object for the active database.
*/
protected function dbDelete($table, $keys) {
$query = db_delete($table, $this->options);
// Change field 'customized' into 'l10n_status'. This enables the Drupal 8
// backported code to work with the Drupal 7 style database tables.
if (isset($keys['customized'])) {
$keys['l10n_status'] = $keys['customized'];
unset($keys['customized']);
}
foreach ($keys as $field => $value) {
$query->condition($field, $value);
}
return $query;
}
/**
* Executes an arbitrary SELECT query string.
*/
protected function dbExecute($query, array $args = array()) {
return db_query($query, $args, $this->options);
}
}

View File

@@ -0,0 +1,180 @@
<?php
/**
* @file
* Definition of StringInterface.
*/
/**
* Defines the locale string interface.
*/
interface StringInterface {
/**
* Gets the string unique identifier.
*
* @return int
* The string identifier.
*/
public function getId();
/**
* Sets the string unique identifier.
*
* @param int $id
* The string identifier.
*
* @return LocaleString
* The called object.
*/
public function setId($id);
/**
* Gets the string version.
*
* @return string
* Version identifier.
*/
public function getVersion();
/**
* Sets the string version.
*
* @param string $version
* Version identifier.
*
* @return LocaleString
* The called object.
*/
public function setVersion($version);
/**
* Gets plain string contained in this object.
*
* @return string
* The string contained in this object.
*/
public function getString();
/**
* Sets the string contained in this object.
*
* @param string $string
* String to set as value.
*
* @return LocaleString
* The called object.
*/
public function setString($string);
/**
* Splits string to work with plural values.
*
* @return array
* Array of strings that are plural variants.
*/
public function getPlurals();
/**
* Sets this string using array of plural values.
*
* Serializes plural variants in one string glued by L10N_UPDATE_PLURAL_DELIMITER.
*
* @param array $plurals
* Array of strings with plural variants.
*
* @return LocaleString
* The called object.
*/
public function setPlurals($plurals);
/**
* Gets the string storage.
*
* @return StringStorageInterface
* The storage used for this string.
*/
public function getStorage();
/**
* Sets the string storage.
*
* @param StringStorageInterface $storage
* The storage to use for this string.
*
* @return LocaleString
* The called object.
*/
public function setStorage($storage);
/**
* Checks whether the object is not saved to storage yet.
*
* @return bool
* TRUE if the object exists in the storage, FALSE otherwise.
*/
public function isNew();
/**
* Checks whether the object is a source string.
*
* @return bool
* TRUE if the object is a source string, FALSE otherwise.
*/
public function isSource();
/**
* Checks whether the object is a translation string.
*
* @return bool
* TRUE if the object is a translation string, FALSE otherwise.
*/
public function isTranslation();
/**
* Sets an array of values as object properties.
*
* @param array $values
* Array with values indexed by property name,
* @param bool $override
* (optional) Whether to override already set fields, defaults to TRUE.
*
* @return LocaleString
* The called object.
*/
public function setValues(array $values, $override = TRUE);
/**
* Gets field values that are set for given field names.
*
* @param array $fields
* Array of field names.
*
* @return array
* Array of field values indexed by field name.
*/
public function getValues(array $fields);
/**
* Saves string object to storage.
*
* @return LocaleString
* The called object.
*
* @throws StringStorageException
* In case of failures, an exception is thrown.
*/
public function save();
/**
* Deletes string object from storage.
*
* @return LocaleString
* The called object.
*
* @throws StringStorageException
* In case of failures, an exception is thrown.
*/
public function delete();
}

View File

@@ -0,0 +1,11 @@
<?php
/**
* @file
* Definition of Drupal\Core\Entity\StringStorageException.
*/
/**
* Defines an exception thrown when storage operations fail.
*/
class StringStorageException extends \Exception { }

View File

@@ -0,0 +1,165 @@
<?php
/**
* @file
* Contains \StringStorageInterface.
*/
/**
* Defines the locale string storage interface.
*/
interface StringStorageInterface {
/**
* Loads multiple source string objects.
*
* @param array $conditions
* (optional) Array with conditions that will be used to filter the strings
* returned and may include any of the following elements:
* - Any simple field value indexed by field name.
* - 'translated', TRUE to get only translated strings or FALSE to get only
* untranslated strings. If not set it returns both translated and
* untranslated strings that fit the other conditions.
* Defaults to no conditions which means that it will load all strings.
* @param array $options
* (optional) An associative array of additional options. It may contain
* any of the following optional keys:
* - 'filters': Array of string filters indexed by field name.
* - 'pager limit': Use pager and set this limit value.
*
* @return array
* Array of \StringInterface objects matching the conditions.
*/
public function getStrings(array $conditions = array(), array $options = array());
/**
* Loads multiple string translation objects.
*
* @param array $conditions
* (optional) Array with conditions that will be used to filter the strings
* returned and may include all of the conditions defined by getStrings().
* @param array $options
* (optional) An associative array of additional options. It may contain
* any of the options defined by getStrings().
*
* @return array
* Array of \StringInterface objects matching the conditions.
*
* @see StringStorageInterface::getStrings()
*/
public function getTranslations(array $conditions = array(), array $options = array());
/**
* Loads a string source object, fast query.
*
* These 'fast query' methods are the ones in the critical path and their
* implementation must be optimized for speed, as they may run many times
* in a single page request.
*
* @param array $conditions
* (optional) Array with conditions that will be used to filter the strings
* returned and may include all of the conditions defined by getStrings().
*
* @return \SourceString|null
* Minimal TranslationString object if found, NULL otherwise.
*/
public function findString(array $conditions);
/**
* Loads a string translation object, fast query.
*
* This function must only be used when actually translating strings as it
* will have the effect of updating the string version. For other purposes
* the getTranslations() method should be used instead.
*
* @param array $conditions
* (optional) Array with conditions that will be used to filter the strings
* returned and may include all of the conditions defined by getStrings().
*
* @return \TranslationString|null
* Minimal TranslationString object if found, NULL otherwise.
*/
public function findTranslation(array $conditions);
/**
* Save string object to storage.
*
* @param \StringInterface $string
* The string object.
*
* @return \StringStorageInterface
* The called object.
*
* @throws \StringStorageException
* In case of failures, an exception is thrown.
*/
public function save($string);
/**
* Delete string from storage.
*
* @param \StringInterface $string
* The string object.
*
* @return \StringStorageInterface
* The called object.
*
* @throws \StringStorageException
* In case of failures, an exception is thrown.
*/
public function delete($string);
/**
* Deletes source strings and translations using conditions.
*
* @param array $conditions
* Array with simple field conditions for source strings.
*/
public function deleteStrings($conditions);
/**
* Deletes translations using conditions.
*
* @param array $conditions
* Array with simple field conditions for string translations.
*/
public function deleteTranslations($conditions);
/**
* Counts source strings.
*
* @return int
* The number of source strings contained in the storage.
*/
public function countStrings();
/**
* Counts translations.
*
* @return array
* The number of translations for each language indexed by language code.
*/
public function countTranslations();
/**
* Creates a source string object bound to this storage but not saved.
*
* @param array $values
* (optional) Array with initial values. Defaults to empty array.
*
* @return \SourceString
* New source string object.
*/
public function createString($values = array());
/**
* Creates a string translation object bound to this storage but not saved.
*
* @param array $values
* (optional) Array with initial values. Defaults to empty array.
*
* @return \TranslationString
* New string translation object.
*/
public function createTranslation($values = array());
}

View File

@@ -0,0 +1,126 @@
<?php
/**
* @file
* Definition of TranslationString.
*/
/**
* Defines the locale translation string object.
*
* This class represents a translation of a source string to a given language,
* thus it must have at least a 'language' which is the language code and a
* 'translation' property which is the translated text of the the source string
* in the specified language.
*/
class TranslationString extends StringBase {
/**
* The language code.
*
* @var string
*/
public $language;
/**
* The string translation.
*
* @var string
*/
public $translation;
/**
* Integer indicating whether this string is customized.
*
* @var int
*/
public $customized;
/**
* Boolean indicating whether the string object is new.
*
* @var bool
*/
protected $is_new;
/**
* Overrides StringBase::__construct().
*/
public function __construct($values = array()) {
parent::__construct($values);
if (!isset($this->is_new)) {
// We mark the string as not new if it is a complete translation.
// This will work when loading from database, otherwise the storage
// controller that creates the string object must handle it.
$this->is_new = !$this->isTranslation();
}
}
/**
* Sets the string as customized / not customized.
*
* @param bool $customized
* (optional) Whether the string is customized or not. Defaults to TRUE.
*
* @return TranslationString
* The called object.
*/
public function setCustomized($customized = TRUE) {
$this->customized = $customized ? L10N_UPDATE_CUSTOMIZED : L10N_UPDATE_NOT_CUSTOMIZED;
return $this;
}
/**
* Implements StringInterface::isSource().
*/
public function isSource() {
return FALSE;
}
/**
* Implements StringInterface::isTranslation().
*/
public function isTranslation() {
return !empty($this->lid) && !empty($this->language) && isset($this->translation);
}
/**
* Implements StringInterface::getString().
*/
public function getString() {
return isset($this->translation) ? $this->translation : '';
}
/**
* Implements StringInterface::setString().
*/
public function setString($string) {
$this->translation = $string;
return $this;
}
/**
* Implements StringInterface::isNew().
*/
public function isNew() {
return $this->is_new;
}
/**
* Implements StringInterface::save().
*/
public function save() {
parent::save();
$this->is_new = FALSE;
return $this;
}
/**
* Implements StringInterface::delete().
*/
public function delete() {
parent::delete();
$this->is_new = TRUE;
return $this;
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* @file
* Definition of TranslationStreamWrapper.
*/
/**
* A Drupal interface translations (translations://) stream wrapper class.
*
* Supports storing translation files.
*/
class TranslationsStreamWrapper extends DrupalLocalStreamWrapper {
/**
* Implements abstract public function getDirectoryPath()
*/
public function getDirectoryPath() {
return variable_get('l10n_update_download_store', L10N_UPDATE_DEFAULT_TRANSLATION_PATH);
}
/**
* Overrides getExternalUrl().
*/
function getExternalUrl() {
throw new Exception('PO files URL should not be public.');
}
}

View File

@@ -0,0 +1,37 @@
(function ($) {
/**
* Show/hide the description details on Available translation updates page.
*/
Drupal.behaviors.hideUpdateInformation = {
attach: function (context, settings) {
var $table = $('#l10n-update-status-form').once('expand-updates');
if ($table.length) {
var $tbodies = $table.find('tbody');
// Open/close the description details by toggling a tr class.
$tbodies.find('.description').bind('click keydown', function (e) {
if (e.keyCode && (e.keyCode !== 13 && e.keyCode !== 32)) {
return;
}
e.preventDefault();
var $tr = $(this).closest('tr');
$tr.toggleClass('expanded');
// Change screen reader text.
$tr.find('.update-description-prefix').text(function () {
if ($tr.hasClass('expanded')) {
return Drupal.t('Hide description');
}
else {
return Drupal.t('Show description');
}
});
});
$table.find('.requirements, .links').hide();
}
}
};
})(jQuery);

View File

@@ -1,22 +0,0 @@
(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);

View File

@@ -0,0 +1,18 @@
<?php
/**
* @file
* Default theme implementation for the last time we checked for update data.
*
* Available variables:
* - $last_checked: User interface string with the formatted time ago when the
* site last checked for available updates.
* - $link: A link to manually check available updates.
*
* @see template_preprocess_l10n_update_last_check()
*
* @ingroup themeable
*/
?>
<div class="l10n_update-checked">
<p><?php print $last_checked; ?> <span class="check-manually">(<?php print $link; ?>)</span></p>
</div>

View File

@@ -0,0 +1,31 @@
<?php
/**
* @file
* Default theme implementation for displaying translation status information.
*
* Displays translation status information per language.
*
* Available variables:
* - module_list: A list of names of modules that have available translation
* updates.
* - details: Rendered list of the translation details.
* - missing_updates_status: If there are any modules that are missing
* translation updates, this variable will contain text indicating how many
* modules are missing translations.
*
* @see template_preprocess_l10n_update_update_info()
*
* @ingroup themeable
*/
?>
<div class="inner" tabindex="0" role="button">
<span class="update-description-prefix visually-hidden">Show description</span>
<?php if($module_list): ?>
<span class="text"><?php print $module_list; ?></span>
<?php elseif($missing_updates_status): ?>
<span class="text"><?php print $missing_updates_status; ?></span>
<?php endif; ?>
<?php if($details): ?>
<div class="details"><?php print drupal_render($details); ?></div>
<?php endif; ?>
</div>

View File

@@ -6,163 +6,205 @@
*/
/**
* Project has a new release available.
* Page callback: Checks for translation updates and displays the status.
*
* Manually checks the translation status without the use of cron.
*/
define('L10N_UPDATE_NOT_CURRENT', 4);
function l10n_update_manual_status() {
module_load_include('compare.inc', 'l10n_update');
/**
* Project is up to date.
*/
define('L10N_UPDATE_CURRENT', 5);
// Check the translation status of all translatable projects in all languages.
// First we clear the cached list of projects. Although not strictly
// necessary, this is helpful in case the project list is out of sync.
l10n_update_flush_projects();
l10n_update_check_projects();
/**
* 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);
// Execute a batch if required. A batch is only used when remote files
// are checked.
if (batch_get()) {
batch_process('admin/config/regional/translate/update');
}
else {
$build['no_projects'] = array('#markup' => t('No projects or languages to update.'));
}
return $build;
drupal_goto('admin/config/regional/translate/update');
}
/**
* 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.
* Page callback: Translation status page.
*/
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');
function l10n_update_status_form() {
module_load_include('compare.inc', 'l10n_update');
$updates = $options = array();
$languages_update = $languages_not_found = array();
$projects_update = array();
// 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();
// @todo Calling l10n_update_build_projects() is an expensive way to
// get a module name. In follow-up issue http://drupal.org/node/1842362
// the project name will be stored to display use, like here.
$project_data = l10n_update_build_projects();
$languages = l10n_update_translatable_language_list();
$status = l10n_update_get_status();
// Prepare information about projects which have available translation
// updates.
if ($languages && $status) {
foreach ($status as $project) {
foreach ($project as $langcode => $project_info) {
if (isset($project_data[$project_info->name])) {
// No translation file found for this project-language combination.
if (empty($project_info->type)) {
$updates[$langcode]['not_found'][] = array(
'name' => $project_info->name == 'drupal' ? t('Drupal core') : $project_data[$project_info->name]->info['name'],
'version' => $project_info->version,
'info' => _l10n_update_status_debug_info($project_info),
);
$languages_not_found[$langcode] = $langcode;
}
// Translation update found for this project-language combination.
elseif ($project_info->type == L10N_UPDATE_LOCAL || $project_info->type == L10N_UPDATE_REMOTE ) {
$local = isset($project_info->files[L10N_UPDATE_LOCAL]) ? $project_info->files[L10N_UPDATE_LOCAL] : NULL;
$remote = isset($project_info->files[L10N_UPDATE_REMOTE]) ? $project_info->files[L10N_UPDATE_REMOTE] : NULL;
$recent = _l10n_update_source_compare($local, $remote) == L10N_UPDATE_SOURCE_COMPARE_LT ? $remote : $local;
$updates[$langcode]['updates'][] = array(
'name' => $project_info->name == 'drupal' ? t('Drupal core') : $project_data[$project_info->name]->info['name'],
'version' => $project_info->version,
'timestamp' => $recent->timestamp,
);
$languages_update[$langcode] = $langcode;
$projects_update[$project_info->name] = $project_info->name;
}
}
}
}
$languages_not_found = array_diff($languages_not_found, $languages_update);
// Build data options for the select table.
foreach($updates as $langcode => $update) {
$title = check_plain($languages[$langcode]);
$l10n_update_update_info = array('#theme' => 'l10n_update_update_info');
foreach (array('updates', 'not_found') as $update_status) {
if (isset($update[$update_status])) {
$l10n_update_update_info['#' . $update_status] = $update[$update_status];
}
}
$options[$langcode] = array(
'title' => array(
'class' => array('label'),
'data' => array(
'#title' => $title,
'#markup' => $title
),
),
'status' => array('class' => array('description', 'expand', 'priority-low'), 'data' => drupal_render($l10n_update_update_info)),
);
}
// Sort the table data on language name.
uasort($options, function ($a, $b) {
return strcasecmp($a['title']['data']['#title'], $b['title']['data']['#title']);
});
}
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'),
$last_checked = variable_get('l10n_update_last_check');
$form['last_checked'] = array(
'#theme' => 'l10n_update_last_check',
'#last' => $last_checked,
);
$header = array(
'title' => array(
'data' => t('Language'),
'class' => array('title'),
),
'status' => array(
'data' => t('Status'),
'class' => array('status', 'priority-low'),
),
);
if (!$languages) {
$empty = t('No translatable languages available. <a href="@add_language">Add a language</a> first.', array('@add_language' => url('admin/config/regional/language')));
}
elseif (empty($options)) {
$empty = t('All translations up to date.');
}
else {
$empty = t('No translation status available. <a href="@check">Check manually</a>.', array('@check' => url('admin/config/regional/translate/check')));
}
// The projects which require an update. Used by the _submit callback.
$form['projects_update'] = array(
'#type' => 'value',
'#value' => $projects_update,
);
$form['langcodes'] = array(
'#type' => 'tableselect',
'#header' => $header,
'#options' => $options,
'#default_value' => $languages_update,
'#empty' => $empty,
'#js_select' => TRUE,
'#multiple' => TRUE,
'#required' => TRUE,
'#not_found' => $languages_not_found,
'#after_build' => array('l10n_update_language_table'),
'#attributes' => array(),
);
$form['#attached'] = array(
'js' => array(
drupal_get_path('module', 'l10n_update') . '/js/l10n_update.admin.js',
),
'css' => array(
drupal_get_path('module', 'l10n_update') . '/css/l10n_update.admin.css',
),
);
if ($languages_update) {
$form['actions'] = array(
'#type' => 'actions',
'submit' => array(
'#type' => 'submit',
'#value' => t('Update translations'),
),
'#attributes' => array(),
);
}
return $form;
}
/**
* Submit handler for Update form.
*
* Handles both submit buttons to update translations and to update the
* form information.
* Form validation handler for locale_translation_status_form().
*/
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');
}
function l10n_update_status_form_validate($form, &$form_state) {
// Check if a language has been selected. 'tableselect' doesn't.
if (!array_filter($form_state['values']['langcodes'])) {
form_set_error('', t('Select a language to update.'));
}
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');
}
/**
* Form submission handler for locale_translation_status_form().
*/
function l10n_update_status_form_submit($form, $form_state) {
module_load_include('fetch.inc', 'l10n_update');
$langcodes = array_filter($form_state['values']['langcodes']);
$projects = array_filter($form_state['values']['projects_update']);
// Set the translation import options. This determines if existing
// translations will be overwritten by imported strings.
$options = _l10n_update_default_update_options();
// If the status was updated recently we can immediately start fetching the
// translation updates. If the status is expired we clear it an run a batch to
// update the status and then fetch the translation updates.
$last_checked = variable_get('l10n_update_last_check');
if ($last_checked < REQUEST_TIME - L10N_UPDATE_STATUS_TTL) {
l10n_update_clear_status();
$batch = l10n_update_batch_update_build(array(), $langcodes, $options);
batch_set($batch);
}
else {
$batch = l10n_update_batch_fetch_build($projects, $langcodes, $options);
batch_set($batch);
}
}
@@ -170,56 +212,80 @@ function l10n_update_admin_import_form_submit($form, $form_state) {
* 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),
'#default_value' => variable_get('l10n_update_check_frequency', '0'),
'#options' => array(
0 => t('Never (manually)'),
1 => t('Daily'),
7 => t('Weekly'),
'0' => t('Never (manually)'),
'7' => t('Weekly'),
'30' => t('Monthly'),
),
'#description' => t('Select how frequently you want to automatically check for updated translations for installed modules and themes.'),
'#description' => t('Select how frequently you want to check for new interface translations for your currently installed modules and themes. <a href="@url">Check updates now</a>.', array('@url' => url('admin/config/regional/translate/check'))),
);
$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.'),
'#default_value' => variable_get('l10n_update_check_disabled', FALSE),
);
$form['l10n_update_check_mode'] = array(
'#type' => 'radios',
'#title' => t('Translation source'),
'#default_value' => variable_get('l10n_update_check_mode', L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL),
'#options' => array(
L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL => t('Drupal translation server and local files'),
L10N_UPDATE_USE_SOURCE_LOCAL => t('Local files only'),
),
'#description' => t('The source of translation files for automatic interface translation.'),
);
$form['l10n_update_download_store'] = array(
'#title' => t('Store downloaded files'),
'#title' => t('Translations directory'),
'#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.'),
'#default_value' => variable_get('l10n_update_download_store', L10N_UPDATE_DEFAULT_TRANSLATION_PATH),
'#required' => TRUE,
'#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.'),
);
$form['l10n_update_import_mode'] = array(
'#type' => 'radios',
'#title' => t('Import behaviour'),
'#default_value' => variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP),
'#options' => array(
LOCALE_IMPORT_KEEP => t("Don't overwrite existing translations."),
L10N_UPDATE_OVERWRITE_NON_CUSTOMIZED => t('Only overwrite imported translations, customized translations are kept.'),
LOCALE_IMPORT_OVERWRITE => t('Overwrite existing translations.'),
),
'#description' => t('How to treat existing translations when automatically updating the interface translations.'),
);
$form['#submit'][] = 'l10n_update_admin_settings_form_submit';
return system_settings_form($form);
}
/**
* Additional validation handler for update settings.
*
* Check for existing files directory and creates one when required.
* Validation handler for translation update settings.
*/
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);
}
// Check for existing translations directory and create one if required.
$directory = $form_state['values']['l10n_update_download_store'];
if (!file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
form_set_error('l10n_update_download_store', t('The directory %directory does not exist or is not writable.', array('%directory' => $directory)));
watchdog('file system', 'The directory %directory does not exist or is not writable.', array('%directory' => $directory), WATCHDOG_ERROR);
}
}
/**
* Submit handler for translation update settings.
*/
function l10n_update_admin_settings_form_submit($form, $form_state) {
// Invalidate the cached translation status when the configuration setting of
// 'l10n_update_check_mode' or 'check_disabled' change.
if ($form['l10n_update_check_mode']['#default_value'] != $form_state['values']['l10n_update_check_mode'] ||
$form['l10n_update_check_disabled']['#default_value'] != $form_state['values']['l10n_update_check_disabled']) {
l10n_update_clear_status();
}
}
@@ -235,316 +301,190 @@ function l10n_update_admin_settings_form_validate($form, &$form_state) {
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'),
L10N_UPDATE_OVERWRITE_NON_CUSTOMIZED => 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.
* Provides debug info for projects in case translation files are not found.
*
* @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.
* Translations files are being fetched either from Drupal translation server
* and local files or only from the local filesystem depending on the
* "Translation source" setting at admin/config/regional/language/update.
* This method will produce debug information including the respective path(s)
* based on this setting.
*
* @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.
* Translations for development versions are never fetched, so the debug info
* for that is a fixed message.
*
* @param array $source
* An array which is the project information of the source.
*
* @return string
* HTML output.
* The string which contains debug information.
*/
function theme_l10n_update_project_status($variables) {
$header = $rows = array();
function _l10n_update_status_debug_info($source) {
$remote_path = isset($source->files['remote']->uri) ? $source->files['remote']->uri : '';
$local_path = isset($source->files['local']->uri) ? $source->files['local']->uri : '';
// 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'],
if (strpos($source->version, 'dev') !== FALSE) {
return t('No translation files are provided for development releases.');
}
if (l10n_update_use_remote_source() && $remote_path && $local_path) {
return t('File not found at %remote_path nor at %local_path', array(
'%remote_path' => $remote_path,
'%local_path' => $local_path,
));
$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'))));
}
elseif ($local_path) {
return t('File not found at %local_path', array('%local_path' => $local_path));
}
// 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;
return t('Translation file location could not be determined.');
}
/**
* Format project translation state with states per language.
* Form element callback: After build changes to the language update table.
*
* @param $variables
* An associative array containing:
* - project: Project data object
* - project_status: Project status
* - languages: Available languages.
* @return string
* HTML output.
* Adds labels to the languages and removes checkboxes from languages from which
* translation files could not be found.
*/
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';
}
function l10n_update_language_table($form_element) {
// Remove checkboxes of languages without updates.
if ($form_element['#not_found']) {
foreach ($form_element['#not_found'] as $langcode) {
$form_element[$langcode] = array();
}
elseif ($available) {
$status = 'available';
$class = 'warning';
}
return $form_element;
}
/**
* Returns HTML for translation edit form.
*
* @param array $variables
* An associative array containing:
* - form: The form that contains the language information.
*
* @see l10n_update_edit_form()
* @ingroup themeable
*/
function theme_l10n_update_edit_form_strings($variables) {
$output = '';
$form = $variables['form'];
$header = array(
t('Source string'),
t('Translation for @language', array('@language' => $form['#language'])),
);
$rows = array();
foreach (element_children($form) as $lid) {
$string = $form[$lid];
if ($string['plural']['#value']) {
$source = drupal_render($string['original_singular']) . '<br />' . drupal_render($string['original_plural']);
}
else {
$status = 'unknown';
$class = 'unknown';
$source = drupal_render($string['original']);
}
// 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));
}
$source .= empty($string['context']) ? '' : '<br /><small>' . t('In Context') . ':&nbsp;' . $string['context']['#value'] . '</small>';
$rows[] = array(
'data' => array($row),
'class' => array($class),
array('data' => $source),
array('data' => $string['translations']),
);
}
$table = array(
'#theme' => 'table',
'#header' => $header,
'#rows' => $rows,
'#empty' => t('No strings available.'),
'#attributes' => array('class' => array('locale-translate-edit-table')),
);
$output .= drupal_render($table);
$pager = array('#theme' => 'pager');
$output .= drupal_render($pager);
return $output;
}
/**
* Prepares variables for translation status information templates.
*
* Translation status information is displayed per language.
*
* Default template: l10n_update-translation-update-info.tpl.php.
*
* @param array $variables
* An associative array containing:
* - updates: The projects which have updates.
* - not_found: The projects which updates are not found.
*
* @see l10n_update_status_form()
*/
function template_preprocess_l10n_update_update_info(&$variables) {
$details = array();
$modules = array();
// Default values
$variables['modules'] = array();
$variables['module_list'] = '';
$details['available_updates_list'] = array();
// Build output for available updates.
if (isset($variables['updates'])) {
$releases = array();
if ($variables['updates']) {
foreach ($variables['updates'] as $update) {
$modules[] = $update['name'];
$releases[] = t('@module (@date)', array('@module' => $update['name'], '@date' => format_date($update['timestamp'], 'html_date')));
}
$variables['modules'] = $modules;
$variables['module_list'] = t('Updates for: @modules', array('@modules' => implode(', ', $modules)));
}
$details['available_updates_list'] = array(
'#theme' => 'item_list',
'#items' => $releases,
);
}
// 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;
// Build output for updates not found.
if (isset($variables['not_found'])) {
$releases = array();
$variables['missing_updates_status'] = format_plural(count($variables['not_found']), 'Missing translations for one project', 'Missing translations for @count projects');
if ($variables['not_found']) {
foreach ($variables['not_found'] as $update) {
$version = $update['version'] ? $update['version'] : t('no version');
$releases[] = t('@module (@version).', array('@module' => $update['name'], '@version' => $version)) . ' ' . $update['info'];
}
}
$details['missing_updates_list'] = array(
'#theme' => 'item_list',
'#items' => $releases,
);
// Prefix the missing updates list if there is an available updates lists
// before it.
if (!empty($details['missing_updates_list']['#items'])) {
$details['missing_updates_list']['#prefix'] = t('Missing translations for:');
}
}
$variables['details'] = $details;
}
/**
* Format a single project translation state.
* Prepares variables for most recent translation update templates.
*
* Displays the last time we checked for locale update data. In addition to
* properly formatting the given timestamp, this function also provides a "Check
* manually" link that refreshes the available update and redirects back to the
* same page.
*
* Default template: l10n_update-translation-last-check.tpl.php.
*
* @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.
* - last: The timestamp when the site last checked for available updates.
*
* @param $variables
* An associative array containing:
* - language: Language name.
* - release: Current file data.
* - status: Release status.
* @return string
* HTML output.
* @see l10n_update_status_form()
*/
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;
function template_preprocess_l10n_update_last_check(&$variables) {
$last = $variables['last'];
$variables['last_checked'] = $last ? t('Last checked: !time ago', array('!time' => format_interval(REQUEST_TIME - $last))) : t('Last checked: never');
$variables['link'] = l(t('Check manually'), 'admin/config/regional/translate/check');
}

View File

@@ -5,27 +5,6 @@
* 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.
*
@@ -46,8 +25,6 @@ function hook_l10n_update_projects_alter(&$projects) {
// 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',
),
);
@@ -60,10 +37,9 @@ function hook_l10n_update_projects_alter(&$projects) {
'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',
'name' => 'New example project',
'version' => '7.x-1.5',
'core' => '7.x',
'l10n path' => 'http://example.com/files/translations/%core/%project/%project-%release.%language.po',
),
);

View File

@@ -2,182 +2,197 @@
/**
* @file
* Reusable API for creating and running l10n update batches.
* Batch process to check the availability of remote or local po files.
*/
// 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.
* Load the common translation API.
*/
function l10n_update_batch_download($updates) {
foreach ($updates as $update) {
$id = $update->filename;
$operations[] = array('_l10n_update_batch_download', array($id, $update));
// @todo Combine functions differently in files to avoid unnecessary includes.
// Follow-up issue http://drupal.org/node/1834298
require_once __DIR__ . '/l10n_update.translation.inc';
/**
* Batch operation callback: Check status of a remote and local po file.
*
* Checks the presence and creation time po translation files in located at
* remote server location and local file system.
*
* @param string $project
* Machine name of the project for which to check the translation status.
* @param string $langcode
* Language code of the language for which to check the translation.
* @param array $options
* Optional, an array with options that can have the following elements:
* - 'finish_feedback': Whether or not to give feedback to the user when the
* batch is finished. Optional, defaults to TRUE.
* - 'use_remote': Whether or not to check the remote translation file.
* Optional, defaults to TRUE.
* @param array $context
* The batch context.
*/
function l10n_update_batch_status_check($project, $langcode, $options = array(), &$context) {
$failure = $checked = FALSE;
$options += array(
'finish_feedback' => TRUE,
'use_remote' => TRUE,
);
$source = l10n_update_get_status(array($project), array($langcode));
$source = $source[$project][$langcode];
// Check the status of local translation files.
if (isset($source->files[L10N_UPDATE_LOCAL])) {
if ($file = l10n_update_source_check_file($source)) {
l10n_update_status_save($source->name, $source->langcode, L10N_UPDATE_LOCAL, $file);
}
$checked = TRUE;
}
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));
// Check the status of remote translation files.
if ($options['use_remote'] && isset($source->files[L10N_UPDATE_REMOTE])) {
$remote_file = $source->files[L10N_UPDATE_REMOTE];
module_load_include('http.inc', 'l10n_update');
if ($result = l10n_update_http_check($remote_file->uri)) {
// Update the file object with the result data. In case of a redirect we
// store the resulting uri.
if (!empty($result->updated)) {
$remote_file->uri = isset($result->redirect_url) ? $result->redirect_url : $remote_file->uri;
$remote_file->timestamp = $result->updated;
l10n_update_status_save($source->name, $source->langcode, L10N_UPDATE_REMOTE, $remote_file);
}
// @todo What to do with when the file is not found (404)? To prevent
// re-checking within the TTL (1day, 1week) we can set a last_checked
// timestamp or cache the result.
$checked = TRUE;
}
else {
$operations[] = array('_l10n_update_batch_import', array($id, $update, $import_mode));
$failure = TRUE;
}
// This one takes always parameters from results.
$operations[] = array('_l10n_update_batch_history', array($id));
}
if (!empty($operations)) {
return _l10n_update_create_batch($operations);
// Provide user feedback and record success or failure for reporting at the
// end of the batch.
if ($options['finish_feedback'] && $checked) {
$context['results']['files'][] = $source->name;
}
if ($failure && !$checked) {
$context['results']['failed_files'][] = $source->name;
}
$context['message'] = t('Checked translation for %project.', array('%project' => $source->project));
}
/**
* Create batch stub for this module.
* Batch finished callback: Set result message.
*
* @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.
* @param boolean $success
* TRUE if batch successfully completed.
* @param array $results
* Batch results.
*/
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);
function l10n_update_batch_status_finished($success, $results) {
if ($success) {
if (isset($results['failed_files'])) {
if (module_exists('dblog')) {
$message = format_plural(count($results['failed_files']), 'One translation file could not be checked. <a href="@url">See the log</a> for details.', '@count translation files could not be checked. <a href="@url">See the log</a> for details.', array('@url' => url('admin/reports/dblog')));
}
else {
$message = format_plural(count($results['failed_files']), 'One translation files could not be checked. See the log for details.', '@count translation files could not be checked. See the log for details.');
}
drupal_set_message($message, 'error');
}
if (isset($results['files'])) {
drupal_set_message(format_plural(
count($results['files']),
'Checked available interface translation updates for one project.',
'Checked available interface translation updates for @count projects.'
));
}
if (!isset($results['failed_files']) && !isset($results['files'])) {
drupal_set_message(t('Nothing to check.'));
}
variable_set('l10n_update_last_check', REQUEST_TIME);
}
else {
$context['results'][$id] = array('file' => $file, 'fail' => TRUE);
drupal_set_message(t('An error occurred trying to check available interface translation updates.'), 'error');
}
}
/**
* Batch process: Update the download history table.
* Batch operation: Download a remote translation file.
*
* @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.
* Downloads a remote gettext file into the translations directory. When
* successfully the translation status is updated.
*
* @param string $project
* Name of the translatable project.
* @param string $langcode
* Language code.
* @param array $context
* The batch context.
*
* @see l10n_update_batch_fetch_import()
*/
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));
function l10n_update_batch_fetch_download($project, $langcode, &$context) {
$sources = l10n_update_get_status(array($project), array($langcode));
if (isset($sources[$project][$langcode])) {
$source = $sources[$project][$langcode];
if (isset($source->type) && $source->type == L10N_UPDATE_REMOTE) {
if ($file = l10n_update_download_source($source->files[L10N_UPDATE_REMOTE], 'translations://')) {
$context['message'] = t('Downloaded translation for %project.', array('%project' => $source->project));
l10n_update_status_save($source->name, $source->langcode, L10N_UPDATE_LOCAL, $file);
}
else {
$context['results']['failed_files'][] = $source->files[L10N_UPDATE_REMOTE];
}
}
}
}
/**
* Batch process: Import translation file.
*
* This takes a file parameter or continues from previous batch
* which should have downloaded a file.
* Imports a gettext file from the translation directory. When successfully the
* translation status is updated.
*
* @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.
* @param string $project
* Name of the translatable project.
* @param string $langcode
* Language code.
* @param array $options
* Array of import options.
* @param array $context
* The batch context.
*
* @see l10n_update_batch_import_files()
* @see l10n_update_batch_fetch_download()
*/
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));
function l10n_update_batch_fetch_import($project, $langcode, $options, &$context) {
$sources = l10n_update_get_status(array($project), array($langcode));
if (isset($sources[$project][$langcode])) {
$source = $sources[$project][$langcode];
if (isset($source->type)) {
if ($source->type == L10N_UPDATE_REMOTE || $source->type == L10N_UPDATE_LOCAL) {
$file = $source->files[L10N_UPDATE_LOCAL];
module_load_include('bulk.inc', 'l10n_update');
$options += array(
'message' => t('Importing translation for %project.', array('%project' => $source->project)),
);
// Import the translation file. For large files the batch operations is
// progressive and will be called repeatedly until finished.
l10n_update_batch_import($file, $options, $context);
// The import is finished.
if (isset($context['finished']) && $context['finished'] == 1) {
// The import is successful.
if (isset($context['results']['files'][$file->uri])) {
$context['message'] = t('Imported translation for %project.', array('%project' => $source->project));
// Save the data of imported source into the {l10n_update_file} table and
// update the current translation status.
l10n_update_status_save($project, $langcode, L10N_UPDATE_CURRENT, $source->files[L10N_UPDATE_LOCAL]);
}
}
}
}
}
}
@@ -185,84 +200,47 @@ function _l10n_update_batch_import($id, $file, $mode, &$context) {
/**
* Batch finished callback: Set result message.
*
* @param $success
* TRUE if batch succesfully completed.
* @param $results
* @param boolean $success
* TRUE if batch successfully completed.
* @param array
* 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();
function l10n_update_batch_fetch_finished($success, $results) {
module_load_include('bulk.inc', 'l10n_update');
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');
variable_set('l10n_update_last_check', REQUEST_TIME);
}
l10n_update_batch_finished($success, $results);
}
/**
* Downloads a translation file from a remote server.
*
* @param object $source_file
* Source file object with at least:
* - "uri": uri to download the file from.
* - "project": Project name.
* - "langcode": Translation language.
* - "version": Project version.
* - "filename": File name.
* @param string $directory
* Directory where the downloaded file will be saved. Defaults to the
* temporary file path.
*
* @return object
* File object if download was successful. FALSE on failure.
*/
function l10n_update_download_source($source_file, $directory = 'temporary://') {
if ($uri = system_retrieve_file($source_file->uri, $directory)) {
$file = clone($source_file);
$file->type = L10N_UPDATE_LOCAL;
$file->uri = $uri;
$file->directory = $directory;
$file->timestamp = filemtime($uri);
return $file;
}
watchdog('l10n_update', 'Unable to download translation file @uri.', array('@uri' => $source_file->uri), WATCHDOG_ERROR);
return FALSE;
}

View File

@@ -0,0 +1,726 @@
<?php
/**
* @file
* Mass import-export and batch import functionality for Gettext .po files.
*/
/**
* Form constructor for the translation import screen.
*
* @see l10n_update_import_form_submit()
* @ingroup forms
*/
function l10n_update_import_form($form, &$form_state) {
drupal_static_reset('language_list');
$languages = language_list();
// Initialize a language list to the ones available, including English if we
// are to translate Drupal to English as well.
$existing_languages = array();
foreach ($languages as $langcode => $language) {
if ($langcode != 'en' || l10n_update_english()) {
$existing_languages[$langcode] = $language->name;
}
}
// If we have no languages available, present the list of predefined languages
// only. If we do have already added languages, set up two option groups with
// the list of existing and then predefined languages.
form_load_include($form_state, 'inc', 'language', 'language.admin');
if (empty($existing_languages)) {
$language_options = language_admin_predefined_list();
$default = key($language_options);
}
else {
$default = key($existing_languages);
$language_options = array(
t('Existing languages') => $existing_languages,
t('Languages not yet added') => language_admin_predefined_list()
);
}
$validators = array(
'file_validate_extensions' => array('po'),
'file_validate_size' => array(file_upload_max_size()),
);
$form['file'] = array(
'#type' => 'file',
'#title' => t('Translation file'),
'#description' => theme('file_upload_help', array('description' => t('A Gettext Portable Object file.'), 'upload_validators' => $validators)),
'#size' => 50,
'#upload_validators' => $validators,
'#attributes' => array('class' => array('file-import-input')),
'#attached' => array(
'js' => array(
drupal_get_path('module', 'locale') . '/locale.bulk.js' => array(),
),
),
);
$form['langcode'] = array(
'#type' => 'select',
'#title' => t('Language'),
'#options' => $language_options,
'#default_value' => $default,
'#attributes' => array('class' => array('langcode-input')),
);
$form['customized'] = array(
'#title' => t('Treat imported strings as custom translations'),
'#type' => 'checkbox',
);
$form['overwrite_options'] = array(
'#type' => 'container',
'#tree' => TRUE,
);
$form['overwrite_options']['not_customized'] = array(
'#title' => t('Overwrite non-customized translations'),
'#type' => 'checkbox',
'#states' => array(
'checked' => array(
':input[name="customized"]' => array('checked' => TRUE),
),
),
);
$form['overwrite_options']['customized'] = array(
'#title' => t('Overwrite existing customized translations'),
'#type' => 'checkbox',
);
$form['actions'] = array(
'#type' => 'actions'
);
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => t('Import')
);
return $form;
}
/**
* Form submission handler for l10n_update_import_form().
*/
function l10n_update_import_form_submit($form, &$form_state) {
// Ensure we have the file uploaded.
if ($file = file_save_upload('file', $form_state, $form['file']['#upload_validators'], 'translations://', 0)) {
// Add language, if not yet supported.
$language = language_load($form_state['values']['langcode']);
if (empty($language)) {
$language = new Language(array(
'id' => $form_state['values']['langcode']
));
$language = language_save($language);
drupal_set_message(t('The language %language has been created.', array('%language' => t($language->name))));
}
$options = array(
'langcode' => $form_state['values']['langcode'],
'overwrite_options' => $form_state['values']['overwrite_options'],
'customized' => $form_state['values']['customized'] ? L10N_UPDATE_CUSTOMIZED : L10N_UPDATE_NOT_CUSTOMIZED,
);
$file = l10n_update_file_attach_properties($file, $options);
$batch = l10n_update_batch_build(array($file->uri => $file), $options);
batch_set($batch);
}
else {
form_set_error('file', $form_state, t('File to import not found.'));
$form_state['rebuild'] = TRUE;
return;
}
$form_state['redirect_route']['route_name'] = 'locale.translate_page';
return;
}
/**
* Form constructor for the Gettext translation files export form.
*
* @see l10n_update_export_form_submit()
* @ingroup forms
*/
function l10n_update_export_form($form, &$form_state) {
global $language;
$languages = language_list();
$language_options = array();
foreach ($languages as $langcode => $language) {
if ($langcode != 'en' || l10n_update_english()) {
$language_options[$langcode] = $language->name;
}
}
$language_default = language_default();
if (empty($language_options)) {
$form['langcode'] = array(
'#type' => 'value',
'#value' => $language->language,
);
$form['langcode_text'] = array(
'#type' => 'item',
'#title' => t('Language'),
'#markup' => t('No language available. The export will only contain source strings.'),
);
}
else {
$form['langcode'] = array(
'#type' => 'select',
'#title' => t('Language'),
'#options' => $language_options,
'#default_value' => $language_default->id,
'#empty_option' => t('Source text only, no translations'),
'#empty_value' => $language->language,
);
$form['content_options'] = array(
'#type' => 'details',
'#title' => t('Export options'),
'#collapsed' => TRUE,
'#tree' => TRUE,
'#states' => array(
'invisible' => array(
':input[name="langcode"]' => array('value' => $language->language),
),
),
);
$form['content_options']['not_customized'] = array(
'#type' => 'checkbox',
'#title' => t('Include non-customized translations'),
'#default_value' => TRUE,
);
$form['content_options']['customized'] = array(
'#type' => 'checkbox',
'#title' => t('Include customized translations'),
'#default_value' => TRUE,
);
$form['content_options']['not_translated'] = array(
'#type' => 'checkbox',
'#title' => t('Include untranslated text'),
'#default_value' => TRUE,
);
}
$form['actions'] = array(
'#type' => 'actions'
);
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => t('Export')
);
return $form;
}
/**
* Form submission handler for l10n_update_export_form().
*/
function l10n_update_export_form_submit($form, &$form_state) {
global $language;
// If template is required, language code is not given.
if ($form_state['values']['langcode'] != $language->language) {
$languages = language_list();
$language = isset($languages[$form_state['values']['langcode']]) ? $languages[$form_state['values']['langcode']] : NULL;
}
else {
$language = NULL;
}
$content_options = isset($form_state['values']['content_options']) ? $form_state['values']['content_options'] : array();
$reader = new PoDatabaseReader();
$languageName = '';
if ($language != NULL) {
$reader->setLangcode($language->id);
$reader->setOptions($content_options);
$languages = language_list();
$languageName = isset($languages[$language->id]) ? $languages[$language->id]->name : '';
$filename = $language->id .'.po';
}
else {
// Template required.
$filename = 'drupal.pot';
}
$item = $reader->readItem();
if (!empty($item)) {
$uri = tempnam('temporary://', 'po_');
$header = $reader->getHeader();
$header->setProjectName(variable_get('site_name'));
$header->setLanguageName($languageName);
$writer = new PoStreamWriter;
$writer->setUri($uri);
$writer->setHeader($header);
$writer->open();
$writer->writeItem($item);
$writer->writeItems($reader);
$writer->close();
}
else {
drupal_set_message('Nothing to export.');
}
}
/**
* Prepare a batch to import all translations.
*
* @param array $options
* An array with options that can have the following elements:
* - 'langcode': The language code. Optional, defaults to NULL, which means
* that the language will be detected from the name of the files.
* - 'overwrite_options': Overwrite options array as defined in
* PoDatabaseWriter. Optional, defaults to an empty array.
* - 'customized': Flag indicating whether the strings imported from $file
* are customized translations or come from a community source. Use
* L10N_UPDATE_CUSTOMIZED or L10N_UPDATE_NOT_CUSTOMIZED. Optional, defaults to
* L10N_UPDATE_NOT_CUSTOMIZED.
* - 'finish_feedback': Whether or not to give feedback to the user when the
* batch is finished. Optional, defaults to TRUE.
*
* @param $force
* (optional) Import all available files, even if they were imported before.
*
* @todo
* Integrate with update status to identify projects needed and integrate
* l10n_update functionality to feed in translation files alike.
* See http://drupal.org/node/1191488.
*/
function l10n_update_batch_import_files($options, $force = FALSE) {
$options += array(
'overwrite_options' => array(),
'customized' => L10N_UPDATE_NOT_CUSTOMIZED,
'finish_feedback' => TRUE,
);
if (!empty($options['langcode'])) {
$langcodes = array($options['langcode']);
}
else {
// If langcode was not provided, make sure to only import files for the
// languages we have enabled.
$langcodes = array_keys(language_list());
}
$files = l10n_update_get_interface_translation_files(array(), $langcodes);
if (!$force) {
$result = db_select('l10n_update_file', 'lf')
->fields('lf', array('langcode', 'uri', 'timestamp'))
->condition('language', $langcodes)
->execute()
->fetchAllAssoc('uri');
foreach ($result as $uri => $info) {
if (isset($files[$uri]) && filemtime($uri) <= $info->timestamp) {
// The file is already imported and not changed since the last import.
// Remove it from file list and don't import it again.
unset($files[$uri]);
}
}
}
return l10n_update_batch_build($files, $options);
}
/**
* Get interface translation files present in the translations directory.
*
* @param array $projects
* Project names from which to get the translation files and history.
* Defaults to all projects.
* @param array $langcodes
* Language codes from which to get the translation files and history.
* Defaults to all languagues
*
* @return array
* An array of interface translation files keyed by their URI.
*/
function l10n_update_get_interface_translation_files($projects = array(), $langcodes = array()) {
module_load_include('compare.inc', 'l10n_update');
$files = array();
$projects = $projects ? $projects : array_keys(l10n_update_get_projects());
$langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
// Scan the translations directory for files matching a name pattern
// containing a project name and language code: {project}.{langcode}.po or
// {project}-{version}.{langcode}.po.
// Only files of known projects and languages will be returned.
$directory = variable_get('l10n_update_download_store', L10N_UPDATE_DEFAULT_TRANSLATION_PATH);
$result = file_scan_directory($directory, '![a-z_]+(\-[0-9a-z\.\-\+]+|)\.[^\./]+\.po$!', array('recurse' => FALSE));
foreach ($result as $file) {
// Update the file object with project name and version from the file name.
$file = l10n_update_file_attach_properties($file);
if (in_array($file->project, $projects)) {
if (in_array($file->langcode, $langcodes)) {
$files[$file->uri] = $file;
}
}
}
return $files;
}
/**
* Build a locale batch from an array of files.
*
* @param $files
* Array of file objects to import.
*
* @param array $options
* An array with options that can have the following elements:
* - 'langcode': The language code. Optional, defaults to NULL, which means
* that the language will be detected from the name of the files.
* - 'overwrite_options': Overwrite options array as defined in
* PoDatabaseWriter. Optional, defaults to an empty array.
* - 'customized': Flag indicating whether the strings imported from $file
* are customized translations or come from a community source. Use
* L10N_UPDATE_CUSTOMIZED or L10N_UPDATE_NOT_CUSTOMIZED. Optional, defaults to
* L10N_UPDATE_NOT_CUSTOMIZED.
* - 'finish_feedback': Whether or not to give feedback to the user when the
* batch is finished. Optional, defaults to TRUE.
*
* @return
* A batch structure or FALSE if $files was empty.
*/
function l10n_update_batch_build($files, $options) {
$options += array(
'overwrite_options' => array(),
'customized' => L10N_UPDATE_NOT_CUSTOMIZED,
'finish_feedback' => TRUE,
);
if (count($files)) {
$operations = array();
foreach ($files as $file) {
// We call l10n_update_batch_import for every batch operation.
$operations[] = array('l10n_update_batch_import', array($file, $options));
}
// Save the translation status of all files.
$operations[] = array('l10n_update_batch_import_save', array());
// Add a final step to refresh JavaScript and configuration strings.
$operations[] = array('l10n_update_batch_refresh', array());
$batch = array(
'operations' => $operations,
'title' => t('Importing interface translations'),
'progress_message' => '',
'error_message' => t('Error importing interface translations'),
'file' => drupal_get_path('module', 'locale') . '/locale.bulk.inc',
);
if ($options['finish_feedback']) {
$batch['finished'] = 'l10n_update_batch_finished';
}
return $batch;
}
return FALSE;
}
/**
* Perform interface translation import as a batch step.
*
* @param object $file
* A file object of the gettext file to be imported. The file object must
* contain a language parameter. This is used as the language of the import.
*
* @param array $options
* An array with options that can have the following elements:
* - 'langcode': The language code.
* - 'overwrite_options': Overwrite options array as defined in
* PoDatabaseWriter. Optional, defaults to an empty array.
* - 'customized': Flag indicating whether the strings imported from $file
* are customized translations or come from a community source. Use
* L10N_UPDATE_CUSTOMIZED or L10N_UPDATE_NOT_CUSTOMIZED. Optional, defaults to
* L10N_UPDATE_NOT_CUSTOMIZED.
* - 'message': Alternative message to display during import. Note, this must
* be sanitized text.
*
* @param $context
* Contains a list of files imported.
*/
function l10n_update_batch_import($file, $options, &$context) {
// Merge the default values in the $options array.
$options += array(
'overwrite_options' => array(),
'customized' => L10N_UPDATE_NOT_CUSTOMIZED,
);
if (isset($file->langcode)) {
try {
if (empty($context['sandbox'])) {
$context['sandbox']['parse_state'] = array(
'filesize' => filesize(drupal_realpath($file->uri)),
'chunk_size' => 200,
'seek' => 0,
);
}
// Update the seek and the number of items in the $options array().
$options['seek'] = $context['sandbox']['parse_state']['seek'];
$options['items'] = $context['sandbox']['parse_state']['chunk_size'];
$report = Gettext::fileToDatabase($file, $options);
// If not yet finished with reading, mark progress based on size and
// position.
if ($report['seek'] < filesize($file->uri)) {
$context['sandbox']['parse_state']['seek'] = $report['seek'];
// Maximize the progress bar at 95% before completion, the batch API
// could trigger the end of the operation before file reading is done,
// because of floating point inaccuracies. See
// http://drupal.org/node/1089472
$context['finished'] = min(0.95, $report['seek'] / filesize($file->uri));
if (isset($options['message'])) {
$context['message'] = t('!message (@percent%).', array('!message' => $options['message'], '@percent' => (int) ($context['finished'] * 100)));
}
else {
$context['message'] = t('Importing translation file: %filename (@percent%).', array('%filename' => $file->filename, '@percent' => (int) ($context['finished'] * 100)));
}
}
else {
// We are finished here.
$context['finished'] = 1;
// Store the file data for processing by the next batch operation.
$file->timestamp = filemtime($file->uri);
$context['results']['files'][$file->uri] = $file;
$context['results']['languages'][$file->uri] = $file->langcode;
}
// Add the reported values to the statistics for this file.
// Each import iteration reports statistics in an array. The results of
// each iteration are added and merged here and stored per file.
if (!isset($context['results']['stats']) || !isset($context['results']['stats'][$file->uri])) {
$context['results']['stats'][$file->uri] = array();
}
foreach ($report as $key => $value) {
if (is_numeric($report[$key])) {
if (!isset($context['results']['stats'][$file->uri][$key])) {
$context['results']['stats'][$file->uri][$key] = 0;
}
$context['results']['stats'][$file->uri][$key] += $report[$key];
}
elseif (is_array($value)) {
$context['results']['stats'][$file->uri] += array($key => array());
$context['results']['stats'][$file->uri][$key] = array_merge($context['results']['stats'][$file->uri][$key], $value);
}
}
}
catch (Exception $exception) {
// Import failed. Store the data of the failing file.
$context['results']['failed_files'][] = $file;
watchdog('l10n_update', 'Unable to import translations file: @file (@message)', array('@file' => $file->uri, '@message' => $exception->getMessage()));
}
}
}
/**
* Batch callback: Save data of imported files.
*
* @param $context
* Contains a list of imported files.
*/
function l10n_update_batch_import_save($context) {
if (isset($context['results']['files'])) {
foreach ($context['results']['files'] as $file) {
// Update the file history if both project and version are known. This
// table is used by the automated translation update function which tracks
// translation status of module and themes in the system. Other
// translation files are not tracked and are therefore not stored in this
// table.
if ($file->project && $file->version) {
$file->last_checked = REQUEST_TIME;
l10n_update_update_file_history($file);
}
}
$context['message'] = t('Translations imported.');
}
}
/**
* Refreshs translations after importing strings.
*
* @param array $context
* Contains a list of strings updated and information about the progress.
*/
function l10n_update_batch_refresh(array &$context) {
if (!isset($context['sandbox']['refresh'])) {
$strings = $langcodes = array();
if (isset($context['results']['stats'])) {
// Get list of unique string identifiers and language codes updated.
$langcodes = array_unique(array_values($context['results']['languages']));
foreach ($context['results']['stats'] as $report) {
$strings = array_merge($strings, $report['strings']);
}
}
if ($strings) {
// Initialize multi-step string refresh.
$context['message'] = t('Updating translations for JavaScript and configuration strings.');
$context['sandbox']['refresh']['strings'] = array_unique($strings);
$context['sandbox']['refresh']['languages'] = $langcodes;
$context['sandbox']['refresh']['names'] = array();
$context['results']['stats']['config'] = 0;
$context['sandbox']['refresh']['count'] = count($strings);
// We will update strings on later steps.
$context['finished'] = 1 - 1 / $context['sandbox']['refresh']['count'];
}
else {
$context['finished'] = 1;
}
}
elseif (!empty($context['sandbox']['refresh']['strings'])) {
// Not perfect but will give some indication of progress.
$context['finished'] = 1 - count($context['sandbox']['refresh']['strings']) / $context['sandbox']['refresh']['count'];
// Pending strings, refresh 100 at a time, get next pack.
$next = array_slice($context['sandbox']['refresh']['strings'], 0, 100);
array_splice($context['sandbox']['refresh']['strings'], 0, count($next));
// Clear cache and force refresh of JavaScript translations.
_l10n_update_refresh_translations($context['sandbox']['refresh']['languages'], $next);
}
else {
$context['finished'] = 1;
}
}
/**
* Finished callback of system page locale import batch.
*/
function l10n_update_batch_finished($success, $results) {
if ($success) {
$additions = $updates = $deletes = $skips = $config = 0;
if (isset($results['failed_files'])) {
if (module_exists('dblog')) {
$message = format_plural(count($results['failed_files']), 'One translation file could not be imported. <a href="@url">See the log</a> for details.', '@count translation files could not be imported. <a href="@url">See the log</a> for details.', array('@url' => url('admin/reports/dblog')));
}
else {
$message = format_plural(count($results['failed_files']), 'One translation files could not be imported. See the log for details.', '@count translation files could not be imported. See the log for details.');
}
drupal_set_message($message, 'error');
}
if (isset($results['files'])) {
$skipped_files = array();
// If there are no results and/or no stats (eg. coping with an empty .po
// file), simply do nothing.
if ($results && isset($results['stats'])) {
foreach ($results['stats'] as $filepath => $report) {
$additions += $report['additions'];
$updates += $report['updates'];
$deletes += $report['deletes'];
$skips += $report['skips'];
if ($report['skips'] > 0) {
$skipped_files[] = $filepath;
}
}
}
drupal_set_message(format_plural(count($results['files']),
'One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.',
'@count translation files imported. %number translations were added, %update translations were updated and %delete translations were removed.',
array('%number' => $additions, '%update' => $updates, '%delete' => $deletes)
));
watchdog('l10n_update', 'Translations imported: %number added, %update updated, %delete removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes));
if ($skips) {
if (module_exists('dblog')) {
$message = format_plural($skips, 'One translation string was skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', '@count translation strings were skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', array('@url' => url('admin/reports/dblog')));
}
else {
$message = format_plural($skips, 'One translation string was skipped because of disallowed or malformed HTML. See the log for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.');
}
drupal_set_message($message, 'warning');
watchdog('l10n_update', '@count disallowed HTML string(s) in files: @files.', array('@count' => $skips, '@files' => implode(',', $skipped_files)), WATCHDOG_WARNING);
}
}
}
}
/**
* Creates a file object and populates the timestamp property.
*
* @param $filepath
* The filepath of a file to import.
*
* @return
* An object representing the file.
*/
function l10n_update_file_create($filepath) {
$file = new stdClass();
$file->filename = drupal_basename($filepath);
$file->uri = $filepath;
$file->timestamp = filemtime($file->uri);
return $file;
}
/**
* Generates file properties from filename and options.
*
* An attempt is made to determine the translation language, project name and
* project version from the file name. Supported file name patterns are:
* {project}-{version}.{langcode}.po, {prefix}.{langcode}.po or {langcode}.po.
* Alternatively the translation language can be set using the $options.
*
* @param object $file
* A file object of the gettext file to be imported.
* @param array $options
* An array with options:
* - 'langcode': The language code. Overrides the file language.
*
* @return object
* Modified file object.
*/
function l10n_update_file_attach_properties($file, $options = array()) {
// If $file is a file entity, convert it to a stdClass.
if ($file instanceof FileInterface) {
$file = (object) array(
'filename' => $file->getFilename(),
'uri' => $file->getFileUri(),
);
}
// Extract project, version and language code from the file name. Supported:
// {project}-{version}.{langcode}.po, {prefix}.{langcode}.po or {langcode}.po
preg_match('!
( # project OR project and version OR emtpy (group 1)
([a-z_]+) # project name (group 2)
\. # .
| # OR
([a-z_]+) # project name (group 3)
\- # -
([0-9a-z\.\-\+]+) # version (group 4)
\. # .
| # OR
) # (empty)
([^\./]+) # language code (group 5)
\. # .
po # po extension
$!x', $file->filename, $matches);
if (isset($matches[5])) {
$file->project = $matches[2] . $matches[3];
$file->version = $matches[4];
$file->langcode = isset($options['langcode']) ? $options['langcode'] : $matches[5];
}
return $file;
}
/**
* Deletes interface translation files and translation history records.
*
* @param array $projects
* Project names from which to delete the translation files and history.
* Defaults to all projects.
* @param array $langcodes
* Language codes from which to delete the translation files and history.
* Defaults to all languagues
*
* @return boolean
* TRUE if files are removed successfully. FALSE if one or more files could
* not be deleted.
*/
function l10n_update_delete_translation_files($projects = array(), $langcodes = array()) {
$fail = FALSE;
l10n_update_file_history_delete($projects, $langcodes);
// Delete all translation files from the translations directory.
if ($files = l10n_update_get_interface_translation_files($projects, $langcodes)) {
foreach ($files as $file) {
$success = file_unmanaged_delete($file->uri);
if (!$success) {
$fail = TRUE;
}
}
}
return !$fail;
}

View File

@@ -1,488 +0,0 @@
<?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);
}
}
}

View File

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

View File

@@ -16,14 +16,14 @@ function l10n_update_drush_command() {
$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.',
'languages' => 'Comma separated list of languages. Defaults to all available languages. Example: --languages="nl, fr, de"',
)
);
$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.'
'languages' => 'Comma separated list of languages. Defaults to all available languages. Example: --languages="nl, fr, de"',
'mode' => 'Determine if existing translations are overwitten during import. Use "overwrite" to overwrite any existing translation, "replace" to replace previously imported translations but not overwrite edited strings, "keep" to keep any existing translation and only add new translations. Default value: "keep"'
),
);
return $commands;
@@ -33,56 +33,66 @@ function l10n_update_drush_command() {
* Callback for command l10n-update-refresh.
*/
function drush_l10n_update_refresh() {
module_load_include('admin.inc', 'l10n_update');
module_load_include('compare.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);
// Check the translation status of all translatable projects in all languages.
// First we clear the cached list of projects. Although not strictly
// necessary, this is helpful in case the project list is out of sync.
l10n_update_flush_projects();
l10n_update_check_projects();
// Execute a batch if required. A batch is only used when remote files
// are checked.
if (batch_get()) {
drush_backend_batch_process();
}
}
/**
* Validate command l10n-update-status.
*/
function drush_l10n_update_status_validate() {
_drush_l10n_update_validate_languages();
return _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)) {
$status = l10n_update_get_status();
if (!empty($status)) {
$languages = drush_get_option('languages');
// Table header.
// Build table header.
$table = array();
$header = array(dt('Project'));
foreach ($languages as $lang => $language) {
$header[] = $language . ' status';
foreach ($languages as $langcode => $language) {
$header[] = $language->name;
}
$table[] = $header;
// Iterate projects to obtain per language status.
$projects = l10n_update_get_projects();
$history = l10n_update_get_history();
foreach ($projects as $name => $project) {
foreach ($status 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');
// First column: Project name & version number.
$project_details = reset($project);
$title = isset($project_details->title) ? $project_details->title : $project_details->name;
$row[] = dt('@project (@version)', array('@project' => $title, '@version' => $project_details->version));
// Other columns: Status per language.
foreach ($languages as $langcode => $language) {
$current = $project[$langcode]->type == L10N_UPDATE_CURRENT;
$local_update = $project[$langcode]->type == L10N_UPDATE_LOCAL;
$remote_update = $project[$langcode]->type == L10N_UPDATE_REMOTE;
if ($local_update || $remote_update) {
$row[] = $remote_update ? dt('Remote update available') : dt('Local update available');
}
elseif ($current) {
$row[] = t('Up to date');
$row[] = dt('Up to date');
}
else {
$row[] = t('No information');
$row[] = dt('No info');
}
}
$table[] = $row;
@@ -90,7 +100,7 @@ function drush_l10n_update_status() {
drush_print_table($table, TRUE);
}
else {
drush_log(dt('No projects or languages to update.'), 'ok');
drush_log(dt('No languages to update.'), 'warning');
}
}
@@ -98,12 +108,14 @@ function drush_l10n_update_status() {
* Validate command l10n-update.
*/
function drush_l10n_update_validate() {
_drush_l10n_update_validate_languages();
$lang_validation = _drush_l10n_update_validate_languages();
if ($lang_validation == FALSE) {
return FALSE;
}
// 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.'));
if (!in_array($mode, array('keep', 'replace', 'overwrite'))) {
return drush_set_error('L10N_UPDATE_INVALID_MODE', dt('Invalid update mode. Valid options are keep, replace, overwrite.'));
}
}
@@ -111,25 +123,52 @@ function drush_l10n_update_validate() {
* Callback for command l10n-update.
*/
function drush_l10n_update() {
module_load_include('fetch.inc', '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');
if ($updates['projects']) {
drush_log(dt('Found @count projects to update.', array('@count' => count($updates['projects']))), 'status');
// Batch update all projects for selected languages.
$mode = drush_get_option('mode', 'keep');
$options = _l10n_update_default_update_options();
switch ($mode) {
case 'keep':
$options['overwrite_options'] = array(
'not_customized' => FALSE,
'customized' => FALSE,
);
break;
case 'replace':
$options['overwrite_options'] = array(
'not_customized' => TRUE,
'customized' => FALSE,
);
break;
case 'overwrite':
$options['overwrite_options'] = array(
'not_customized' => TRUE,
'customized' => TRUE,
);
break;
default:
return drush_set_error('L10N_UPDATE_INVALID_MODE', dt('Invalid update mode. Valid options are keep, overwrite.'));
break;
}
$languages = array_keys(drush_get_option('languages'));
// Get translation status of the projects, download and update translations.
$batch = l10n_update_batch_update_build(array(), $languages, $options);
drush_log($batch['title'], 'status');
drush_log($batch['init_message'], 'status');
batch_set($batch);
drush_backend_batch_process();
}
else {
drush_log(dt('All project translations up to date'), 'status');
}
}
@@ -141,32 +180,43 @@ function drush_l10n_update() {
* 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();
// Check if there are installed languages other than english.
$installed_languages = l10n_update_translatable_language_list();
// Indicate that there's nothing to do, only show a warning.
if (empty($installed_languages)) {
return drush_set_error('L10N_UPDATE_NO_LANGUAGES', dt('No languages to update.'));
drush_log(dt('No languages to update.'), 'warning');
return FALSE;
}
// 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)));
foreach ($languages as $key => $langcode) {
if (!isset($installed_languages[$langcode])) {
if (is_numeric($langcode)) {
drush_set_error('L10N_UPDATE_INVALID_LANGUAGE', dt('Invalid language "@langcode". Use for example: --languages="nl, fr, de"', array('@langcode' => $langcode)));
}
else {
drush_set_error('L10N_UPDATE_INVALID_LANGUAGE', dt('Language "@langcode" is not installed.', array('@langcode' => $langcode)));
}
}
else {
unset($languages[$key]);
$languages[$lang] = $installed_languages[$lang];
$languages[$langcode] = $installed_languages[$langcode];
}
}
if (drush_get_error() != DRUSH_SUCCESS) {
drush_print(dt('Available languages: @languages', array('@languages' => implode(', ', array_keys($installed_languages)))));
return FALSE;
}
}
else {
$languages = $installed_languages;
}
drush_set_option('languages', $languages);
return TRUE;
}
/**
@@ -175,16 +225,29 @@ function _drush_l10n_update_validate_languages() {
* @return $updates array or NULL.
*/
function _drush_l10n_update_get_updates() {
$projects = l10n_update_get_projects();
if ($projects) {
$history = l10n_update_get_history();
$updates = array();
$languages = l10n_update_translatable_language_list();
$status = l10n_update_get_status();
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);
// Prepare information about projects which have available translation
// updates.
if ($languages && $status) {
foreach ($status as $project) {
foreach ($project as $langcode => $project_info) {
// Translation update found for this project-language combination.
if ($project_info->type && ($project_info->type == L10N_UPDATE_LOCAL || $project_info->type == L10N_UPDATE_REMOTE)) {
$updates['projects'][$project_info->name] = $project_info;
$updates['languages'][$langcode] = $project_info;
}
}
}
}
if ($updates) {
return $updates;
}
else {
drush_log(dt('No projects or languages to update.'), 'ok');
drush_log(dt('No languages to update.'), 'warning');
}
}

View File

@@ -0,0 +1,108 @@
<?php
/**
* @file
* The API for download and import of translations from remote and local sources.
*/
/**
* Load the common translation API.
*/
// @todo Combine functions differently in files to avoid unnecessary includes.
// Follow-up issue http://drupal.org/node/1834298
require_once __DIR__ . '/l10n_update.translation.inc';
/**
* Builds a batch to check, download and import project translations.
*
* @param array $projects
* Array of project names for which to update the translations. Defaults to
* all translatable projects.
* @param array $langcodes
* Array of language codes. Defaults to all translatable languages.
* @param array $options
* Array of import options. See locale_translate_batch_import_files().
*
* @return array
* Batch definition array.
*/
function l10n_update_batch_update_build($projects = array(), $langcodes = array(), $options = array()) {
module_load_include('compare.inc', 'l10n_update');
$projects = $projects ? $projects : array_keys(l10n_update_get_projects());
$langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
$status_options = $options;
$status_options['finish_feedback'] = FALSE;
// Check status of local and remote translation files.
$operations = _l10n_update_batch_status_operations($projects, $langcodes, $status_options);
// Download and import translations.
$operations = array_merge($operations, _l10n_update_fetch_operations($projects, $langcodes, $options));
$batch = array(
'operations' => $operations,
'title' => t('Updating translations'),
'progress_message' => '',
'error_message' => t('Error importing translation files'),
'finished' => 'l10n_update_batch_fetch_finished',
'file' => drupal_get_path('module', 'l10n_update') . '/l10n_update.batch.inc',
);
return $batch;
}
/**
* Builds a batch to download and import project translations.
*
* @param array $projects
* Array of project names for which to check the state of translation files.
* Defaults to all translatable projects.
* @param array $langcodes
* Array of language codes. Defaults to all translatable languages.
* @param array $options
* Array of import options. See l10n_update_batch_import_files().
*
* @return array
* Batch definition array.
*/
function l10n_update_batch_fetch_build($projects = array(), $langcodes = array(), $options = array()) {
$projects = $projects ? $projects : array_keys(l10n_update_get_projects());
$langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
$batch = array(
'operations' => _l10n_update_fetch_operations($projects, $langcodes, $options),
'title' => t('Updating translations.'),
'progress_message' => '',
'error_message' => t('Error importing translation files'),
'finished' => 'l10n_update_batch_fetch_finished',
'file' => drupal_get_path('module', 'l10n_update') . '/l10n_update.batch.inc',
);
return $batch;
}
/**
* Helper function to construct the batch operations to fetch translations.
*
* @param array $projects
* Array of project names for which to check the state of translation files.
* Defaults to all translatable projects.
* @param array $langcodes
* Array of language codes. Defaults to all translatable languages.
* @param array $options
* Array of import options.
*
* @return array
* Array of batch operations.
*/
function _l10n_update_fetch_operations($projects, $langcodes, $options) {
$operations = array();
foreach ($projects as $project) {
foreach ($langcodes as $langcode) {
if (l10n_update_use_remote_source()) {
$operations[] = array('l10n_update_batch_fetch_download', array($project, $langcode));
}
$operations[] = array('l10n_update_batch_fetch_import', array($project, $langcode, $options));
}
}
return $operations;
}

View File

@@ -2,248 +2,9 @@
/**
* @file
* Reusable API for l10n remote updates.
* Http API for l10n 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.
*
@@ -258,8 +19,29 @@ function l10n_update_flag_history($available) {
*/
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;
if (!isset($result->error)) {
if ($result && $result->code == 200) {
$result->updated = isset($result->headers['last-modified']) ? strtotime($result->headers['last-modified']) : 0;
}
return $result;
}
else {
switch ($result->code) {
case 404:
// File not found occurs when a translation file is not yet available
// at the translation server. But also if a custom module or custom
// theme does not define the location of a translation file. By default
// the file is checked at the translation server, but it will not be
// found there.
watchdog('l10n_update', 'File not found: @uri.', array('@uri' => $url));
return TRUE;
case 0:
watchdog('l10n_update', 'Error occurred when trying to check @remote: @errormessage.', array('@errormessage' => $result->error, '@remote' => $url), WATCHDOG_ERROR);
break;
default:
watchdog('l10n_update', 'HTTP error @errorcode occurred when trying to check @remote.', array('@errorcode' => $result->code, '@remote' => $url), WATCHDOG_ERROR);
break;
}
}
return $result;
}
@@ -297,7 +79,8 @@ function l10n_update_http_check($url, $headers = array()) {
* received.
* - redirect_code: If redirected, an integer containing the initial response
* status code.
* - redirect_url: If redirected, a string containing the redirection location.
* - redirect_url: If redirected, a string containing the URL of the redirect
* target.
* - 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
@@ -333,10 +116,51 @@ function l10n_update_http_request($url, array $options = array()) {
'timeout' => 30.0,
'context' => NULL,
);
// Merge the default headers.
$options['headers'] += array(
'User-Agent' => 'Drupal (+http://drupal.org/)',
);
// stream_socket_client() requires timeout to be a float.
$options['timeout'] = (float) $options['timeout'];
// Use a proxy if one is defined and the host is not on the excluded list.
$proxy_server = variable_get('proxy_server', '');
if ($proxy_server && _drupal_http_use_proxy($uri['host'])) {
// Set the scheme so we open a socket to the proxy server.
$uri['scheme'] = 'proxy';
// Set the path to be the full URL.
$uri['path'] = $url;
// Since the URL is passed as the path, we won't use the parsed query.
unset($uri['query']);
// Add in username and password to Proxy-Authorization header if needed.
if ($proxy_username = variable_get('proxy_username', '')) {
$proxy_password = variable_get('proxy_password', '');
$options['headers']['Proxy-Authorization'] = 'Basic ' . base64_encode($proxy_username . (!empty($proxy_password) ? ":" . $proxy_password : ''));
}
// Some proxies reject requests with any User-Agent headers, while others
// require a specific one.
$proxy_user_agent = variable_get('proxy_user_agent', '');
// The default value matches neither condition.
if ($proxy_user_agent === NULL) {
unset($options['headers']['User-Agent']);
}
elseif ($proxy_user_agent) {
$options['headers']['User-Agent'] = $proxy_user_agent;
}
}
switch ($uri['scheme']) {
case 'proxy':
// Make the socket connection to a proxy server.
$socket = 'tcp://' . $proxy_server . ':' . variable_get('proxy_port', 8080);
// The Host header still needs to match the real request.
$options['headers']['Host'] = $uri['host'];
$options['headers']['Host'] .= isset($uri['port']) && $uri['port'] != 80 ? ':' . $uri['port'] : '';
break;
case 'http':
case 'feed':
$port = isset($uri['port']) ? $uri['port'] : 80;
@@ -346,12 +170,14 @@ function l10n_update_http_request($url, array $options = array()) {
// 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;
@@ -376,7 +202,7 @@ function l10n_update_http_request($url, array $options = array()) {
// 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()
// See system_requirements().
// variable_set('drupal_http_request_fails', TRUE);
return $result;
@@ -388,11 +214,6 @@ function l10n_update_http_request($url, array $options = array()) {
$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
@@ -404,7 +225,7 @@ function l10n_update_http_request($url, array $options = array()) {
// 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'] : ''));
$options['headers']['Authorization'] = 'Basic ' . base64_encode($uri['user'] . (isset($uri['pass']) ? ':' . $uri['pass'] : ''));
}
// If the database prefix is being used by SimpleTest to run the tests in a copied
@@ -459,7 +280,9 @@ function l10n_update_http_request($url, array $options = array()) {
return $result;
}
// Parse response headers from the response body.
list($response, $result->data) = explode("\r\n\r\n", $response, 2);
// Be tolerant of malformed HTTP responses that separate header and body with
// \n\n or \r\r instead of \r\n\r\n.
list($response, $result->data) = preg_split("/\r\n\r\n|\n\n|\r\r/", $response, 2);
$response = preg_split("/\r\n|\n|\r/", $response);
// Parse the response status line.
@@ -551,7 +374,9 @@ function l10n_update_http_request($url, array $options = array()) {
$result = l10n_update_http_request($location, $options);
$result->redirect_code = $code;
}
$result->redirect_url = $location;
if (!isset($result->redirect_url)) {
$result->redirect_url = $location;
}
break;
default:
$result->error = $status_message;
@@ -559,29 +384,3 @@ function l10n_update_http_request($url, array $options = array()) {
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);
}

View File

@@ -4,21 +4,35 @@ 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
files[] = includes/gettext/PoHeader.php
files[] = includes/gettext/PoItem.php
files[] = includes/gettext/PoMemoryWriter.php
files[] = includes/gettext/PoMetadataInterface.php
files[] = includes/gettext/PoReaderInterface.php
files[] = includes/gettext/PoStreamInterface.php
files[] = includes/gettext/PoStreamReader.php
files[] = includes/gettext/PoStreamWriter.php
files[] = includes/gettext/PoWriterInterface.php
; Information added by drupal.org packaging script on 2012-02-06
version = "7.x-1.0-beta3"
files[] = includes/locale/Gettext.php
files[] = includes/locale/PoDatabaseReader.php
files[] = includes/locale/PoDatabaseWriter.php
files[] = includes/locale/SourceString.php
files[] = includes/locale/StringBase.php
files[] = includes/locale/StringDatabaseStorage.php
files[] = includes/locale/StringInterface.php
files[] = includes/locale/StringStorageException.php
files[] = includes/locale/StringStorageInterface.php
files[] = includes/locale/TranslationString.php
files[] = includes/locale/TranslationsStreamWrapper.php
files[] = tests/L10nUpdateCronTest.test
files[] = tests/L10nUpdateInterfaceTest.test
files[] = tests/L10nUpdateTest.test
files[] = tests/L10nUpdateTestBase.test
; Information added by Drupal.org packaging script on 2014-11-10
version = "7.x-2.0"
core = "7.x"
project = "l10n_update"
datestamp = "1328563848"
datestamp = "1415625781"

View File

@@ -53,7 +53,7 @@ function l10n_update_schema() {
'default' => '',
),
'status' => array(
'description' => 'Status flag. TBD',
'description' => 'Status flag. If TRUE, translations of this module will be updated.',
'type' => 'int',
'not null' => TRUE,
'default' => 1,
@@ -150,6 +150,7 @@ function l10n_update_schema_alter(&$schema) {
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'Boolean indicating whether the translation is custom to this site.',
);
}
@@ -159,6 +160,22 @@ function l10n_update_schema_alter(&$schema) {
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);
// Create the translation directory. We try different alternative paths as the
// default may not always be writable.
$directories = array(
variable_get('l10n_update_download_store', L10N_UPDATE_DEFAULT_TRANSLATION_PATH),
variable_get('file_public_path', conf_path() . '/files') . '/translations',
'sites/default/files/translations',
);
foreach ($directories as $directory) {
if (file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
variable_set('l10n_update_download_store', $directory);
return;
}
}
watchdog('l10n_update', 'The directory %directory does not exist or is not writable.', array('%directory' => $directories[0]), WATCHDOG_ERROR);
drupal_set_message(t('The directory %directory does not exist or is not writable.', array('%directory' => $directories[0])), 'error');
}
/**
@@ -168,77 +185,85 @@ 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_filename');
variable_del('l10n_update_default_update_url');
variable_del('l10n_update_download_store');
variable_del('l10n_update_import_enabled');
variable_del('l10n_update_import_mode');
variable_del('l10n_update_rebuild_projects');
}
variable_del('l10n_update_check_frequency');
variable_del('l10n_update_last_check');
variable_del('l10n_update_download_store');
variable_del('l10n_update_translation_status');
variable_del('l10n_update_check_mode');}
/**
* Implements hook_requirements().
*/
function l10n_update_requirements($phase) {
$requirements = array();
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')
)
);
$available_updates = array();
$untranslated = array();
$languages = l10n_update_translatable_language_list();
if ($languages) {
// Determine the status of the translation updates per language.
$status = l10n_update_get_status();
if ($status) {
foreach ($status as $project) {
foreach ($project as $langcode => $project_info) {
if (empty($project_info->type)) {
$untranslated[$langcode] = $languages[$langcode];
}
elseif ($project_info->type == L10N_UPDATE_LOCAL || $project_info->type == L10N_UPDATE_REMOTE) {
$available_updates[$langcode] = $languages[$langcode];
}
}
}
if ($available_updates || $untranslated) {
if ($available_updates) {
$requirements['l10n_update'] = array(
'title' => 'Translation update status',
'value' => l(t('Updates available'), 'admin/config/regional/translate/update'),
'severity' => REQUIREMENT_WARNING,
'description' => t('Updates available for: @languages. See the <a href="!updates">Available translation updates</a> page for more information.', array('@languages' => implode(', ', $available_updates), '!updates' => url('admin/config/regional/translate/update'))),
);
}
else {
$requirements['l10n_update'] = array(
'title' => 'Translation update status',
'value' => t('Missing translations'),
'severity' => REQUIREMENT_INFO,
'description' => t('Missing translations for: @languages. See the <a href="!updates">Available translation updates</a> page for more information.', array('@languages' => implode(', ', $untranslated), '!updates' => url('admin/config/regional/translate/update'))),
);
}
}
else {
$requirements['l10n_update'] = array(
'title' => 'Translation update status',
'value' => t('Up to date'),
'severity' => REQUIREMENT_OK,
);
}
}
else {
$requirements['l10n_update']['severity'] = REQUIREMENT_OK;
$requirements['l10n_update']['value'] = t('All your translations are up to date');
$requirements['locale_translation'] = array(
'title' => 'Translation update status',
'value' => l(t('Can not determine status'), 'admin/config/regional/translate/update'),
'severity' => REQUIREMENT_WARNING,
'description' => t('No translation status is available. See the <a href="!updates">Available translation updates</a> page for more information.', array('!updates' => url('admin/config/regional/translate/update'))),
);
}
}
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();
}
if ($phase == 'update') {
// Make sure the 'translations' stream wrapper class gets registered.
// This is needed when upgrading to 7.x-2.x.
if (!class_exists('TranslationsStreamWrapper')) {
registry_rebuild();
}
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.');
return $requirements;
}
/**
@@ -282,3 +307,59 @@ function l10n_update_update_7004() {
db_create_table('cache_l10n_update', $schema);
}
}
/**
* Migration to 7.x-2.x branch.
*/
function l10n_update_update_7200() {
// Make sure the 'translations' stream wrapper class gets registered.
if (!class_exists('TranslationsStreamWrapper')) {
registry_rebuild();
}
if (!variable_get('l10n_update_download_store', '')) {
variable_set('l10n_update_download_store', 'sites/all/translations');
}
// Create the translation directory. We try different alternative paths as the
// default may not always be writable.
$directories = array(
variable_get('l10n_update_download_store', L10N_UPDATE_DEFAULT_TRANSLATION_PATH),
variable_get('file_public_path', conf_path() . '/files') . '/translations',
'sites/default/files/translations',
);
foreach ($directories as $directory) {
if (file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
variable_set('l10n_update_download_store', $directory);
return;
}
}
watchdog('l10n_update', 'The directory %directory does not exist or is not writable.', array('%directory' => $directories[0]), WATCHDOG_ERROR);
drupal_set_message(t('The directory %directory does not exist or is not writable.', array('%directory' => $directories[0])), 'error');
// Translation source 'Remote server only' is no longer supported. Use 'Remote
// and local' instead.
$mode = variable_get('l10n_update_check_mode', 3); // L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL
if ($mode == 1) {
variable_set('l10n_update_check_mode', 3); // L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL
}
// Daily cron updates are no longer supported. Use weekly instead.
$frequency = variable_get('l10n_update_check_frequency', '0');
if ($frequency == '1') {
variable_set('l10n_update_check_frequency', '7');
}
// Clean up deprecated variables.
variable_del('l10n_update_default_server');
variable_del('l10n_update_default_server_url');
variable_del('l10n_update_rebuild_projects');
}
/**
* Sets the default translation files directory.
*/
function l10n_update_update_7201() {
if (!variable_get('l10n_update_download_store', '')) {
variable_set('l10n_update_download_store', 'sites/all/translations');
}
}

View File

@@ -1,463 +0,0 @@
<?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;
}

View File

@@ -3,41 +3,102 @@
/**
* @file
* Download translations from remote localization server.
*/
/**
* Translation update mode: Use local files only.
*
* @todo Fetch information from info files.
* When checking for available translation updates, only local files will be
* used. Any remote translation file will be ignored. Also custom modules and
* themes which have set a "server pattern" to use a remote translation server
* will be ignored.
*/
define('L10N_UPDATE_USE_SOURCE_LOCAL', 2);
/**
* Update mode: Remote server.
* Translation update mode: Use both remote and local files.
*
* When checking for available translation updates, both local and remote files
* will be checked.
*/
define('L10N_UPDATE_CHECK_REMOTE', 1);
define('L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL', 3);
/**
* Update mode: Local server.
* Default location of gettext file on the translation server.
*
* @see l10n_update_default_translation_server().
*/
define('L10N_UPDATE_CHECK_LOCAL', 2);
define('L10N_UPDATE_DEFAULT_SERVER_PATTERN', 'http://ftp.drupal.org/files/translations/%core/%project/%project-%release.%language.po');
/**
* Update mode: both.
* Default gettext file name on the translation server.
*/
define('L10N_UPDATE_CHECK_ALL', L10N_UPDATE_CHECK_REMOTE | L10N_UPDATE_CHECK_LOCAL);
define('L10N_UPDATE_DEFAULT_FILE_NAME', '%project-%release.%language.po');
/**
* Translation import mode keeping translations which are edited after enabling
* Locale Update module an only override default (un-edited) translations.
* Default gettext file name on the translation server.
*/
define('LOCALE_UPDATE_OVERRIDE_DEFAULT', 2);
define('L10N_UPDATE_DEFAULT_TRANSLATION_PATH', 'sites/all/translations');
/**
* The maximum number of projects which are checked for available translations each cron run.
* The number of seconds that the translations status entry should be considered.
*/
define('L10N_UPDATE_CRON_PROJECTS', 10);
define('L10N_UPDATE_STATUS_TTL', 600);
/**
* The maximum number of projects which are updated each cron run.
* UI option for override of existing translations. Only override non-customized
* translations.
*/
define('L10N_UPDATE_CRON_UPDATES', 2);
define('L10N_UPDATE_OVERWRITE_NON_CUSTOMIZED', 2);
/**
* Translation source is a remote file.
*/
define('L10N_UPDATE_REMOTE', 'remote');
/**
* Translation source is a local file.
*/
define('L10N_UPDATE_LOCAL', 'local');
/**
* Translation source is the current translation.
*/
define('L10N_UPDATE_CURRENT', 'current');
/**
* The delimiter used to split plural strings.
*
* This is the ETX (End of text) character and is used as a minimal means to
* separate singular and plural variants in source and translation text. It
* was found to be the most compatible delimiter for the supported databases.
*/
define('L10N_UPDATE_PLURAL_DELIMITER', "\03");
/**
* Flag for locally not customized interface translation.
*
* Such translations are imported from .po files downloaded from
* localize.drupal.org for example.
*/
define('L10N_UPDATE_NOT_CUSTOMIZED', 0);
/**
* Flag for locally customized interface translation.
*
* Strings are customized when translated or edited using the build in
* string translation form. Strings can also be marked as customized when a po
* file is imported.
*/
define('L10N_UPDATE_STRING_CUSTOM', 1);
/**
* Flag for locally customized interface translation.
*
* Such translations are edited from their imported originals on the user
* interface or are imported as customized.
*/
define('L10N_UPDATE_CUSTOMIZED', 1);
/**
* Implements hook_help().
@@ -45,8 +106,8 @@ define('L10N_UPDATE_CRON_UPDATES', 2);
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>';
$output = '<p>' . t('Status of interface translations for each of the enabled languages.') . '</p>';
$output .= '<p>' . t('If there are available updates you can click on "Update translation" 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':
@@ -63,12 +124,22 @@ function l10n_update_menu() {
$items['admin/config/regional/translate/update'] = array(
'title' => 'Update',
'description' => 'Available updates',
'page callback' => 'l10n_update_admin_overview',
'page callback' => 'drupal_get_form',
'page arguments' => array('l10n_update_status_form'),
'access arguments' => array('translate interface'),
'file' => 'l10n_update.admin.inc',
'weight' => 20,
'type' => MENU_LOCAL_TASK,
);
$items['admin/config/regional/translate/check'] = array(
'title' => 'Update',
'description' => 'Available updates',
'page callback' => 'l10n_update_manual_status',
'access arguments' => array('translate interface'),
'file' => 'l10n_update.admin.inc',
'weight' => 20,
'type' => MENU_CALLBACK,
);
$items['admin/config/regional/language/update'] = array(
'title' => 'Translation updates',
'description' => 'Automatic update configuration',
@@ -82,12 +153,31 @@ function l10n_update_menu() {
return $items;
}
/**
* Implements hook_theme().
*/
function l10n_update_theme() {
return array(
'l10n_update_last_check' => array(
'variables' => array('last' => NULL),
'file' => 'l10n_update.admin.inc',
'template' => 'l10n_update-translation-last-check',
),
'l10n_update_update_info' => array(
'variables' => array('updates' => array(), 'not_found' => array()),
'file' => 'l10n_update.admin.inc',
'template' => 'l10n_update-translation-update-info',
),
);
}
/**
* 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';
if (module_exists('l10n_client')) {
$menu['l10n_client/save']['page callback'] = 'l10n_update_client_save_string';
}
}
/**
@@ -96,21 +186,110 @@ function l10n_update_menu_alter(&$menu) {
* 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)));
// Update translations only when an update frequency was set by the admin
// and a translatable language was set.
// Update tasks are added to the queue here but processed by Drupal's cron
// using the cron worker defined in l10n_update_queue_info().
if ($frequency = variable_get('l10n_update_check_frequency', '0') && l10n_update_translatable_language_list()) {
module_load_include('translation.inc', 'l10n_update');
l10n_update_cron_fill_queue();
}
}
/**
* Implements hook_cron_queue_info().
*/
function l10n_update_cron_queue_info() {
$queues['l10n_update'] = array(
'worker callback' => 'l10n_update_worker',
'time' => 30,
);
return $queues;
}
/**
* Callback: Executes interface translation queue tasks.
*
* The translation update functions executed here are batch operations which
* are also used in translation update batches. The batch functions may need to
* be executed multiple times to complete their task, typically this is the
* translation import function. When a batch function is not finished, a new
* queue task is created and added to the end of the queue. The batch context
* data is needed to continue the batch task is stored in the queue with the
* queue data.
*
* @param array $data
* Queue data array containing:
* - Function name.
* - Array of function arguments. Optionally contains the batch context data.
*
* @see l10n_update_queue_info()
*/
function l10n_update_worker($data) {
module_load_include('batch.inc', 'l10n_update');
list($function, $args) = $data;
// We execute batch operation functions here to check, download and import the
// translation files. Batch functions use a context variable as last argument
// which is passed by reference. When a batch operation is called for the
// first time a default batch context is created. When called iterative
// (usually the batch import function) the batch context is passed through via
// the queue and is part of the $data.
$last = count($args) - 1;
if (!is_array($args[$last]) || !isset($args[$last]['finished'])) {
$batch_context = array(
'sandbox' => array(),
'results' => array(),
'finished' => 1,
'message' => '',
);
}
else {
$batch_context = $args[$last];
unset ($args[$last]);
}
$args = array_merge($args, array(&$batch_context));
// Call the batch operation function.
call_user_func_array($function, $args);
// If the batch operation is not finished we create a new queue task to
// continue the task. This is typically the translation import task.
if ($batch_context['finished'] < 1) {
unset($batch_context['strings']);
$queue = DrupalQueue::get('l10n_update', TRUE);
$queue->createItem(array($function, $args));
}
}
/**
* Implements hook_stream_wrappers().
*/
function l10n_update_stream_wrappers() {
// Load the stream wrapper class if not automatically loaded. This happens
// before update.php is executed.
if (!class_exists('TranslationsStreamWrapper')) {
require_once('includes/locale/TranslationsStreamWrapper.php');
}
$wrappers['translations'] = array(
'name' => t('Translation files'),
'class' => 'TranslationsStreamWrapper',
'description' => t('Translation files.'),
'type' => STREAM_WRAPPERS_LOCAL_HIDDEN,
);
return $wrappers;
}
/**
* 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');
case 'i18n_string_locale_translate_edit_form':
$form['#submit'][] = 'l10n_update_locale_translate_edit_form_submit';
break;
case 'locale_languages_predefined_form':
case 'locale_languages_custom_form':
@@ -129,8 +308,24 @@ function l10n_update_form_alter(&$form, $form_state, $form_id) {
* 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);
$components['module'] = $modules;
l10n_update_system_update($components);
}
/**
* Implements hook_modules_disabled().
*
* Set disabled modules to be ignored when updating translations.
*/
function l10n_update_modules_disabled($modules) {
if (!variable_get('l10n_update_check_disabled', FALSE)) {
db_update('l10n_update_project')
->fields(array(
'status' => 0,
))
->condition('name', $modules)
->execute();
}
}
/**
@@ -140,29 +335,40 @@ function l10n_update_modules_enabled($modules) {
* 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();
$components['module'] = $modules;
l10n_update_system_remove($components);
}
/**
* Aditional submit handler for language forms
* Implements hook_themes_enabled().
*
* Refresh project translation status and get translations if required.
*/
function l10n_update_themes_enabled($themes) {
$components['theme'] = $themes;
l10n_update_system_update($components);
}
/**
* Additional 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));
if (variable_get('l10n_update_import_enabled', TRUE)) {
if (empty($form_state['values']['predefined_langcode']) || $form_state['values']['predefined_langcode'] == 'custom') {
$langcode = $form_state['values']['langcode'];
}
else {
$langcode = $form_state['values']['predefined_langcode'];
}
// Download and import translations for the newly added language.
module_load_include('fetch.inc', 'l10n_update');
$options = _l10n_update_default_update_options();
$batch = l10n_update_batch_update_build(array(), array($langcode), $options);
batch_set($batch);
}
}
/**
@@ -172,67 +378,28 @@ function l10n_update_languages_changed_submit($form, $form_state) {
*/
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);
l10n_update_file_history_delete(array(), $langcode);
}
/**
* Replacement submit handler for translation edit form.
* Additional submit handler for locale and i18n_string translation edit form.
*
* Process string editing form submissions marking translations as customized.
* Saves all translations of one string submitted from a form.
* Mark locally edited translations as customized.
*
* @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')
foreach ($form_state['values']['translations'] as $langcode => $value) {
if (!empty($value) && $value != $form_state['complete form']['translations'][$langcode]['#default_value']) {
// An update has been made, mark the string as customized.
db_update('locales_target')
->fields(array('l10n_status' => L10N_UPDATE_STRING_CUSTOM))
->condition('lid', $lid)
->condition('language', $key)
->condition('language', $langcode)
->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;
}
/**
@@ -246,9 +413,9 @@ function l10n_update_client_save_string() {
// 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');
module_load_include('translation.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);
@@ -296,232 +463,299 @@ function l10n_update_client_save_string() {
}
/**
* Get stored list of projects
* Imports translations when new modules or themes are installed.
*
* @param boolean $refresh
* TRUE = refresh the project data.
* @param boolean $disabled
* TRUE = get enabled AND disabled projects.
* FALSE = get enabled projects only.
* This function will start a batch to import translations for the added
* components.
*
* @return array
* Array of project objects keyed by project name.
* @param array $components
* An array of arrays of component (theme and/or module) names to import
* translations for, indexed by type.
*/
function l10n_update_get_projects($refresh = FALSE, $disabled = FALSE) {
static $projects, $enabled;
function l10n_update_system_update(array $components) {
$components += array('module' => array(), 'theme' => array());
$list = array_merge($components['module'], $components['theme']);
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;
}
// Skip running the translation imports if in the installer,
// because it would break out of the installer flow. We have
// built-in support for translation imports in the installer.
if (!drupal_installation_attempted() && l10n_update_translatable_language_list() && variable_get('l10n_update_import_enabled', TRUE)) {
module_load_include('compare.inc', 'l10n_update');
// Update the list of translatable projects and start the import batch.
// Only when new projects are added the update batch will be triggered. Not
// each enabled module will introduce a new project. E.g. sub modules.
$projects = array_keys(l10n_update_build_projects());
if ($list = array_intersect($list, $projects)) {
module_load_include('fetch.inc', 'l10n_update');
// Get translation status of the projects, download and update translations.
$options = _l10n_update_default_update_options();
$batch = l10n_update_batch_update_build($list, array(), $options);
batch_set($batch);
}
}
return $disabled ? $projects : $enabled;
}
/**
* Get server information, that can come from different sources.
* Delete translation history of modules and themes.
*
* - 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
* Only the translation history is removed, not the source strings or
* translations. This is not possible because strings are shared between
* modules and we have no record of which string is used by which module.
*
* @param string $name
* Server name e.g. localize.drupal.org
* @param string $url
* Server url
* @param boolean $refresh
* TRUE = refresh the server data.
* @param array $components
* An array of arrays of component (theme and/or module) names to import
* translations for, indexed by type.
*/
function l10n_update_system_remove($components) {
$components += array('module' => array(), 'theme' => array());
$list = array_merge($components['module'], $components['theme']);
if ($language_list = l10n_update_translatable_language_list()) {
module_load_include('compare.inc', 'l10n_update');
module_load_include('bulk.inc', 'l10n_update');
// Only when projects are removed, the translation files and records will be
// deleted. Not each disabled module will remove a project. E.g. sub modules.
$projects = array_keys(l10n_update_get_projects());
if ($list = array_intersect($list, $projects)) {
l10n_update_file_history_delete($list);
// Remove translation files.
l10n_update_delete_translation_files($list, array());
// Remove translatable projects.
// Followup issue http://drupal.org/node/1842362 to replace the
// {l10n_update_project} table. Then change this to a function call.
db_delete('l10n_update_project')
->condition('name', $list)
->execute();
// Clear the translation status.
l10n_update_status_delete_projects($list);
}
}
}
/**
* Gets current translation status from the {l10n_update_file} table.
*
* @return array
* Array of server data.
* Array of translation file objects.
*/
function l10n_update_server($name = NULL, $url = NULL, $refresh = FALSE) {
static $info, $server_list;
function l10n_update_get_file_history() {
$history = &drupal_static(__FUNCTION__, array());
// 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;
if (empty($history)) {
// Get file history from the database.
$result = db_query('SELECT project, language, filename, version, uri, timestamp, last_checked FROM {l10n_update_file}');
foreach ($result as $file) {
$file->langcode = $file->language;
$file->type = $file->timestamp ? L10N_UPDATE_CURRENT : '';
$history[$file->project][$file->langcode] = $file;
}
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];
}
return $history;
}
/**
* Updates the {locale_file} table.
*
* @param object $file
* Object representing the file just imported.
*
* @return integer
* FALSE on failure. Otherwise SAVED_NEW or SAVED_UPDATED.
*
* @see drupal_write_record()
*/
function l10n_update_update_file_history($file) {
// Update or write new record.
if (db_query("SELECT project FROM {l10n_update_file} WHERE project = :project AND language = :langcode", array(':project' => $file->project, ':langcode' => $file->langcode))->fetchField()) {
$update = array('project', 'language');
}
else {
$update = array();
}
$file->language = $file->langcode;
$result = drupal_write_record('l10n_update_file', $file, $update);
// The file history has changed, flush the static cache now.
// @todo Can we make this more fine grained?
drupal_static_reset('l10n_update_get_file_history');
return $result;
}
/**
* Deletes the history of downloaded translations.
*
* @param array $projects
* Project name(s) to be deleted from the file history. If both project(s) and
* language code(s) are specified the conditions will be ANDed.
* @param array $langcode
* Language code(s) to be deleted from the file history.
*/
function l10n_update_file_history_delete($projects = array(), $langcodes = array()) {
$query = db_delete('l10n_update_file');
if (!empty($projects)) {
$query->condition('project', $projects);
}
if (!empty($langcodes)) {
$query->condition('language', $langcodes);
}
$query->execute();
}
/**
* Gets the current translation status.
*
* @todo What is 'translation status'?
*/
function l10n_update_get_status($projects = NULL, $langcodes = NULL) {
$result = array();
$status = variable_get('l10n_update_translation_status', array());
module_load_include('translation.inc', 'l10n_update');
$projects = $projects ? $projects : array_keys(l10n_update_get_projects());
$langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
// Get the translation status of each project-language combination. If no
// status was stored, a new translation source is created.
foreach ($projects as $project) {
foreach ($langcodes as $langcode) {
if (isset($status[$project][$langcode])) {
$result[$project][$langcode] = $status[$project][$langcode];
}
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;
$sources = l10n_update_build_sources(array($project), array($langcode));
if (isset($sources[$project][$langcode])) {
$result[$project][$langcode] = $sources[$project][$langcode];
}
}
// 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];
return $result;
}
/**
* Implements hook_l10n_servers().
* Saves the status of translation sources in static cache.
*
* @return array
* Array of server data:
* 'name' => server name
* 'server_url' => server url
* 'update_url' => update url
* @param string $project
* Machine readable project name.
* @param string $langcode
* Language code.
* @param string $type
* Type of data to be stored.
* @param array $data
* File object also containing timestamp when the translation is last updated.
*/
function l10n_update_l10n_servers() {
module_load_include('inc', 'l10n_update');
$server = l10n_update_default_server();
return array($server['name'] => $server);
}
function l10n_update_status_save($project, $langcode, $type, $data) {
// Followup issue: http://drupal.org/node/1842362
// Split status storage per module/language and expire individually. This will
// improve performance for large sites.
/**
* 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;
// Load the translation status or build it if not already available.
module_load_include('translation.inc', 'l10n_update');
$status = l10n_update_get_status();
if (empty($status)) {
$projects = l10n_update_get_projects(array($project));
if (isset($projects[$project])) {
$status[$project][$langcode] = l10n_update_source_build($projects[$project], $langcode);
}
}
return $status;
// Merge the new status data with the existing status.
if (isset($status[$project][$langcode])) {
switch ($type) {
case L10N_UPDATE_REMOTE:
case L10N_UPDATE_LOCAL:
// Add the source data to the status array.
$status[$project][$langcode]->files[$type] = $data;
// Check if this translation is the most recent one. Set timestamp and
// data type of the most recent translation source.
if (isset($data->timestamp) && $data->timestamp) {
if ($data->timestamp > $status[$project][$langcode]->timestamp) {
$status[$project][$langcode]->timestamp = $data->timestamp;
$status[$project][$langcode]->last_checked = REQUEST_TIME;
$status[$project][$langcode]->type = $type;
}
}
break;
case L10N_UPDATE_CURRENT:
$data->last_checked = REQUEST_TIME;
$status[$project][$langcode]->timestamp = $data->timestamp;
$status[$project][$langcode]->last_checked = $data->last_checked;
$status[$project][$langcode]->type = $type;
l10n_update_update_file_history($data);
break;
}
variable_set('l10n_update_translation_status', $status);
variable_set('l10n_update_last_check', REQUEST_TIME);
}
}
/**
* Get language list.
* Delete language entries from the status cache.
*
* @param array $langcodes
* Language code(s) to be deleted from the cache.
*/
function l10n_update_status_delete_languages($langcodes) {
if ($status = l10n_update_get_status()) {
foreach ($status as $project => $languages) {
foreach ($languages as $langcode => $source) {
if (in_array($langcode, $langcodes)) {
unset($status[$project][$langcode]);
}
}
}
variable_set('l10n_update_translation_status', $status);
}
}
/**
* Delete project entries from the status cache.
*
* @param array $projects
* Project name(s) to be deleted from the cache.
*/
function l10n_update_status_delete_projects($projects) {
$status = l10n_update_get_status();
foreach ($status as $project => $languages) {
if (in_array($project, $projects)) {
unset($status[$project]);
}
}
variable_set('l10n_update_translation_status', $status);
}
/**
* Returns list of translatable languages.
*
* @return array
* Array of installed language names. English is the source language and
* is therefore not included.
* Array of enabled languages keyed by language name. English is omitted.
*/
function l10n_update_language_list() {
function l10n_update_translatable_language_list() {
$languages = locale_language_list('name');
// Skip English language
if (isset($languages['en'])) {
unset($languages['en']);
}
unset($languages['en']);
return $languages;
}
/**
* Implements hook_theme().
* Clear the translation status cache.
*/
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',
),
);
function l10n_update_clear_status() {
variable_del('l10n_update_translation_status');
variable_del('l10n_update_last_check');
}
/**
* Build the warning message for when there is no data about available updates.
* Checks whether remote translation sources are used.
*
* @return sting
* Message text with links.
* @return bool
* Returns TRUE if remote translations sources should be taken into account
* when checking or importing translation files, FALSE otherwise.
*/
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();
function l10n_update_use_remote_source() {
return variable_get('l10n_update_check_mode', L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL) == L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL;
}

View File

@@ -1,134 +0,0 @@
<?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;
}
}
}
}

View File

@@ -1,243 +0,0 @@
<?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;
}

View File

@@ -0,0 +1,570 @@
<?php
/**
* @file
* Common API for interface translation.
*/
/**
* Comparison result of source files timestamps.
*
* Timestamp of source 1 is less than the timestamp of source 2.
* @see _l10n_update_source_compare()
*/
define('L10N_UPDATE_SOURCE_COMPARE_LT', -1);
/**
* Comparison result of source files timestamps.
*
* Timestamp of source 1 is equal to the timestamp of source 2.
* @see _l10n_update_source_compare()
*/
define('L10N_UPDATE_SOURCE_COMPARE_EQ', 0);
/**
* Comparison result of source files timestamps.
*
* Timestamp of source 1 is greater than the timestamp of source 2.
* @see _l10n_update_source_compare()
*/
define('L10N_UPDATE_SOURCE_COMPARE_GT', 1);
/**
* Get array of projects which are available for interface translation.
*
* This project data contains all projects which will be checked for available
* interface translations.
*
* For full functionality this function depends on Update module.
* When Update module is enabled the project data will contain the most recent
* module status; both in enabled status as in version. When Update module is
* disabled this function will return the last known module state. The status
* will only be updated once Update module is enabled.
*
* @params array $project_names
* Array of names of the projects to get.
*
* @return array
* Array of project data for translation update.
*
* @see l10n_update_build_projects()
*/
function l10n_update_get_projects($project_names = array()) {
$projects = &drupal_static(__FUNCTION__, array());
if (empty($projects)) {
// Get project data from the database.
$result = db_query('SELECT name, project_type, core, version, l10n_path as server_pattern, status FROM {l10n_update_project}');
// http://drupal.org/node/1777106 is a follow-up issue to make the check for
// possible out-of-date project information more robust.
if ($result->rowCount() == 0) {
module_load_include('compare.inc', 'l10n_update');
// At least the core project should be in the database, so we build the
// data if none are found.
l10n_update_build_projects();
$result = db_query('SELECT name, project_type, core, version, l10n_path as server_pattern, status FROM {l10n_update_project}');
}
foreach ($result as $project) {
$projects[$project->name] = $project;
}
}
// Return the requested project names or all projects.
if ($project_names) {
return array_intersect_key($projects, drupal_map_assoc($project_names));
}
return $projects;
}
/**
* Clears the projects cache.
*/
function l10n_update_clear_cache_projects() {
drupal_static('l10n_update_get_projects', array());
}
/**
* Loads cached translation sources containing current translation status.
*
* @param array $projects
* Array of project names. Defaults to all translatable projects.
* @param array $langcodes
* Array of language codes. Defaults to all translatable languages.
*
* @return array
* Array of source objects. Keyed with <project name>:<language code>.
*
* @see l10n_update_source_build()
*/
function l10n_update_load_sources($projects = NULL, $langcodes = NULL) {
$sources = array();
$projects = $projects ? $projects : array_keys(l10n_update_get_projects());
$langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
// Load source data from l10n_update_status cache.
$status = l10n_update_get_status();
// Use only the selected projects and languages for update.
foreach($projects as $project) {
foreach ($langcodes as $langcode) {
$sources[$project][$langcode] = isset($status[$project][$langcode]) ? $status[$project][$langcode] : NULL;
}
}
return $sources;
}
/**
* Build translation sources.
*
* @param array $projects
* Array of project names. Defaults to all translatable projects.
* @param array $langcodes
* Array of language codes. Defaults to all translatable languages.
*
* @return array
* Array of source objects. Keyed by project name and language code.
*
* @see l10n_update_source_build()
*/
function l10n_update_build_sources($projects = array(), $langcodes = array()) {
$sources = array();
$projects = l10n_update_get_projects($projects);
$langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
foreach ($projects as $project) {
foreach ($langcodes as $langcode) {
$source = l10n_update_source_build($project, $langcode);
$sources[$source->name][$source->langcode] = $source;
}
}
return $sources;
}
/**
* Checks whether a po file exists in the local filesystem.
*
* It will search in the directory set in the translation source. Which defaults
* to the "translations://" stream wrapper path. The directory may contain any
* valid stream wrapper.
*
* The "local" files property of the source object contains the definition of a
* po file we are looking for. The file name defaults to
* %project-%release.%language.po. Per project this value can be overridden
* using the server_pattern directive in the module's .info.yml file or by using
* hook_l10n_update_projects_alter().
*
* @param object $source
* Translation source object.
*
* @return stdClass
* Source file object of the po file, updated with:
* - "uri": File name and path.
* - "timestamp": Last updated time of the po file.
* FALSE if the file is not found.
*
* @see l10n_update_source_build()
*/
function l10n_update_source_check_file($source) {
if (isset($source->files[L10N_UPDATE_LOCAL])) {
$source_file = $source->files[L10N_UPDATE_LOCAL];
$directory = $source_file->directory;
$filename = '/' . preg_quote($source_file->filename) . '$/';
if ($files = file_scan_directory($directory, $filename, array('key' => 'name', 'recurse' => FALSE))) {
$file = current($files);
$source_file->uri = $file->uri;
$source_file->timestamp = filemtime($file->uri);
return $source_file;
}
}
return FALSE;
}
/**
* Builds abstract translation source.
*
* @param object $project
* Project object.
* @param string $langcode
* Language code.
* @param string $filename
* File name of translation file. May contain placeholders.
*
* @return object
* Source object:
* - "project": Project name.
* - "name": Project name (inherited from project).
* - "language": Language code.
* - "core": Core version (inherited from project).
* - "version": Project version (inherited from project).
* - "project_type": Project type (inherited from project).
* - "files": Array of file objects containing properties of local and remote
* translation files.
* Other processes can add the following properties:
* - "type": Most recent translation source found. L10N_UPDATE_REMOTE and
* L10N_UPDATE_LOCAL indicate available new translations,
* L10N_UPDATE_CURRENT indicate that the current translation is them
* most recent. "type" sorresponds with a key of the "files" array.
* - "timestamp": The creation time of the "type" translation (file).
* - "last_checked": The time when the "type" translation was last checked.
* The "files" array can hold file objects of type:
* L10N_UPDATE_LOCAL, L10N_UPDATE_REMOTE and
* L10N_UPDATE_CURRENT. Each contains following properties:
* - "type": The object type (L10N_UPDATE_LOCAL,
* L10N_UPDATE_REMOTE, etc. see above).
* - "project": Project name.
* - "langcode": Language code.
* - "version": Project version.
* - "uri": Local or remote file path.
* - "directory": Directory of the local po file.
* - "filename": File name.
* - "timestamp": Timestamp of the file.
* - "keep": TRUE to keep the downloaded file.
*/
function l10n_update_source_build($project, $langcode, $filename = NULL) {
// Create a source object with data of the project object.
$source = clone $project;
$source->project = $project->name;
$source->langcode = $langcode;
$source->type = '';
$source->timestamp = 0;
$source->last_checked = 0;
$filename = $filename ? $filename : variable_get('l10n_update_default_filename', L10N_UPDATE_DEFAULT_FILE_NAME);
// If the server_pattern contains a remote file path we will check for a
// remote file. The local version of this file will only be checked if a
// translations directory has been defined. If the server_pattern is a local
// file path we will only check for a file in the local file system.
$files = array();
if (_l10n_update_file_is_remote($source->server_pattern)) {
$files[L10N_UPDATE_REMOTE] = (object) array(
'project' => $project->name,
'langcode' => $langcode,
'version' => $project->version,
'type' => L10N_UPDATE_REMOTE,
'filename' => l10n_update_build_server_pattern($source, basename($source->server_pattern)),
'uri' => l10n_update_build_server_pattern($source, $source->server_pattern),
);
$files[L10N_UPDATE_LOCAL] = (object) array(
'project' => $project->name,
'langcode' => $langcode,
'version' => $project->version,
'type' => L10N_UPDATE_LOCAL,
'filename' => l10n_update_build_server_pattern($source, $filename),
'directory' => 'translations://',
);
$files[L10N_UPDATE_LOCAL]->uri = $files[L10N_UPDATE_LOCAL]->directory . $files[L10N_UPDATE_LOCAL]->filename;
}
else {
$files[L10N_UPDATE_LOCAL] = (object) array(
'project' => $project->name,
'langcode' => $langcode,
'version' => $project->version,
'type' => L10N_UPDATE_LOCAL,
'filename' => l10n_update_build_server_pattern($source, basename($source->server_pattern)),
'directory' => l10n_update_build_server_pattern($source, drupal_dirname($source->server_pattern)),
);
$files[L10N_UPDATE_LOCAL]->uri = $files[L10N_UPDATE_LOCAL]->directory . '/' . $files[L10N_UPDATE_LOCAL]->filename;
}
$source->files = $files;
// If this project+language is already translated, we add its status and
// update the current translation timestamp and last_updated time. If the
// project+language is not translated before, create a new record.
$history = l10n_update_get_file_history();
if (isset($history[$project->name][$langcode]) && $history[$project->name][$langcode]->timestamp) {
$source->files[L10N_UPDATE_CURRENT] = $history[$project->name][$langcode];
$source->type = L10N_UPDATE_CURRENT;
$source->timestamp = $history[$project->name][$langcode]->timestamp;
$source->last_checked = $history[$project->name][$langcode]->last_checked;
}
else {
l10n_update_update_file_history($source);
}
return $source;
}
/**
* Build path to translation source, out of a server path replacement pattern.
*
* @param object $project
* Project object containing data to be inserted in the template.
* @param string $template
* String containing placeholders. Available placeholders:
* - "%project": Project name.
* - "%release": Project version.
* - "%core": Project core version.
* - "%language": Language code.
*
* @return string
* String with replaced placeholders.
*/
function l10n_update_build_server_pattern($project, $template) {
$variables = array(
'%project' => $project->name,
'%release' => $project->version,
'%core' => $project->core,
'%language' => isset($project->langcode) ? $project->langcode : '%language',
);
return strtr($template, $variables);
}
/**
* Populate a queue with project to check for translation updates.
*/
function l10n_update_cron_fill_queue() {
$updates = array();
// Determine which project+language should be updated.
$last = REQUEST_TIME - variable_get('l10n_update_check_frequency', '0') * 3600 * 24;
$query = db_select('l10n_update_file', 'f');
$query->join('l10n_update_project', 'p', 'p.name = f.project');
$query->condition('f.last_checked', $last, '<');
$query->fields('f', array('project', 'language'));
// Only currently installed / enabled components should be checked for.
$query->condition('p.status', 1);
$files = $query->execute()->fetchAll();
foreach ($files as $file) {
$updates[$file->project][] = $file->language;
// Update the last_checked timestamp of the project+language that will
// be checked for updates.
db_update('l10n_update_file')
->fields(array('last_checked' => REQUEST_TIME))
->condition('project', $file->project)
->condition('language', $file->language)
->execute();
}
// For each project+language combination a number of tasks are added to
// the queue.
if ($updates) {
module_load_include('fetch.inc', 'l10n_update');
$options = _l10n_update_default_update_options();
$queue = DrupalQueue::get('l10n_update', TRUE);
foreach ($updates as $project => $languages) {
$batch = l10n_update_batch_update_build(array($project), $languages, $options);
foreach ($batch['operations'] as $item) {
$queue->createItem($item);
}
}
}
}
/**
* Determine if a file is a remote file.
*
* @param string $uri
* The URI or URI pattern of the file.
*
* @return boolean
* TRUE if the $uri is a remote file.
*/
function _l10n_update_file_is_remote($uri) {
$scheme = file_uri_scheme($uri);
if ($scheme) {
return !drupal_realpath($scheme . '://');
}
return FALSE;
}
/**
* Compare two update sources, looking for the newer one.
*
* The timestamp property of the source objects are used to determine which is
* the newer one.
*
* @param object $source1
* Source object of the first translation source.
* @param object $source2
* Source object of available update.
*
* @return integer
* - "L10N_UPDATE_SOURCE_COMPARE_LT": $source1 < $source2 OR $source1
* is missing.
* - "L10N_UPDATE_SOURCE_COMPARE_EQ": $source1 == $source2 OR both
* $source1 and $source2 are missing.
* - "L10N_UPDATE_SOURCE_COMPARE_EQ": $source1 > $source2 OR $source2
* is missing.
*/
function _l10n_update_source_compare($source1, $source2) {
if (isset($source1->timestamp) && isset($source2->timestamp)) {
if ($source1->timestamp == $source2->timestamp) {
return L10N_UPDATE_SOURCE_COMPARE_EQ;
}
else {
return $source1->timestamp > $source2->timestamp ? L10N_UPDATE_SOURCE_COMPARE_GT : L10N_UPDATE_SOURCE_COMPARE_LT;
}
}
elseif (isset($source1->timestamp) && !isset($source2->timestamp)) {
return L10N_UPDATE_SOURCE_COMPARE_GT;
}
elseif (!isset($source1->timestamp) && isset($source2->timestamp)) {
return L10N_UPDATE_SOURCE_COMPARE_LT;
}
else {
return L10N_UPDATE_SOURCE_COMPARE_EQ;
}
}
/**
* Returns default import options for translation update.
*
* @return array
* Array of translation import options.
*/
function _l10n_update_default_update_options() {
$options = array(
'customized' => L10N_UPDATE_NOT_CUSTOMIZED,
'finish_feedback' => TRUE,
'use_remote' => l10n_update_use_remote_source(),
);
switch (variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP)) {
case LOCALE_IMPORT_OVERWRITE:
$options['overwrite_options'] = array(
'customized' => TRUE,
'not_customized' => TRUE,
);
break;
case L10N_UPDATE_OVERWRITE_NON_CUSTOMIZED:
$options['overwrite_options'] = array(
'customized' => FALSE,
'not_customized' => TRUE,
);
break;
case LOCALE_IMPORT_KEEP:
$options['overwrite_options'] = array(
'customized' => FALSE,
'not_customized' => FALSE,
);
break;
}
return $options;
}
/**
* 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_NOT_CUSTOMIZED, $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_NOT_CUSTOMIZED && $mode == L10N_UPDATE_OVERWRITE_NON_CUSTOMIZED) || $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;
}

View File

@@ -0,0 +1,115 @@
<?php
/**
* @file
* Contains L10nUpdateCronTest.
*/
/**
* Tests for translation update using cron.
*/
class L10nUpdateCronTest extends L10nUpdateTestBase {
protected $batch_output = array();
public static function getInfo() {
return array(
'name' => 'Update translations using cron',
'description' => 'Tests for using cron to update project interface translations.',
'group' => 'Localization Update',
);
}
function setUp() {
parent::setUp();
$admin_user = $this->drupalCreateUser(array('administer modules', 'administer site configuration', 'administer languages', 'access administration pages', 'translate interface'));
$this->drupalLogin($admin_user);
$this->addLanguage('de');
}
/**
* Tests interface translation update using cron.
*/
function testUpdateCron() {
// Set a flag to let the l10n_update_test module replace the project data
// with a set of test projects.
variable_set('l10n_update_test_projects_alter', TRUE);
// Setup local and remote translations files.
$this->setTranslationFiles();
variable_set('l10n_update_default_filename', '%project-%release.%language._po');
// Update translations using batch to ensure a clean test starting point.
$this->drupalGet('admin/config/regional/translate/check');
$this->drupalPost('admin/config/regional/translate/update', array(), t('Update translations'));
// Store translation status for comparison.
$initial_history = l10n_update_get_file_history();
// Prepare for test: Simulate new translations being available.
// Change the last updated timestamp of a translation file.
$contrib_module_two_uri = 'public://local/contrib_module_two-7.x-2.0-beta4.de._po';
touch(drupal_realpath($contrib_module_two_uri), REQUEST_TIME);
// Prepare for test: Simulate that the file has not been checked for a long
// time. Set the last_check timestamp to zero.
$query = db_update('l10n_update_file');
$query->fields(array('last_checked' => 0));
$query->condition('project', 'contrib_module_two');
$query->condition('language', 'de');
$query->execute();
// Test: Disable cron update and verify that no tasks are added to the
// queue.
$edit = array(
'l10n_update_check_frequency' => '0',
);
$this->drupalPost('admin/config/regional/language/update', $edit, t('Save configuration'));
// Execute l10n_update cron taks to add tasks to the queue.
l10n_update_cron();
// Check whether no tasks are added to the queue.
$queue = DrupalQueue::get('l10n_update', TRUE);
$this->assertEqual($queue->numberOfItems(), 0, 'Queue is empty');
// Test: Enable cron update and check if update tasks are added to the
// queue.
// Set cron update to Weekly.
$edit = array(
'l10n_update_check_frequency' => '7',
);
$this->drupalPost('admin/config/regional/language/update', $edit, t('Save configuration'));
// Execute l10n_update cron task to add tasks to the queue.
l10n_update_cron();
// Check whether tasks are added to the queue.
$queue = DrupalQueue::get('l10n_update', TRUE);
$this->assertEqual($queue->numberOfItems(), 3, 'Queue holds tasks for one project.');
$item = $queue->claimItem();
$queue->releaseItem($item);
$this->assertEqual($item->data[1][0], 'contrib_module_two', 'Queue holds tasks for contrib module one.');
// Test: Run cron for a second time and check if tasks are not added to
// the queue twice.
l10n_update_cron();
// Check whether no more tasks are added to the queue.
$queue = DrupalQueue::get('l10n_update', TRUE);
$this->assertEqual($queue->numberOfItems(), 3, 'Queue holds tasks for one project.');
// Ensure last checked is updated to a greater time than the initial value.
sleep(1);
// Test: Execute cron and check if tasks are executed correctly.
// Run cron to process the tasks in the queue.
$this->drupalGet('admin/reports/status/run-cron');
drupal_static_reset('l10n_update_get_file_history');
$history = l10n_update_get_file_history();
$initial = $initial_history['contrib_module_two']['de'];
$current = $history['contrib_module_two']['de'];
$this->assertTrue($current->timestamp > $initial->timestamp, 'Timestamp is updated');
$this->assertTrue($current->last_checked > $initial->last_checked, 'Last checked is updated');
}
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* @file
* Contains L10nUpdateInterfaceTest.
*/
/**
* Tests for the l10n_update status user interfaces.
*/
class L10nUpdateInterfaceTest extends L10nUpdateTestBase {
public static function getInfo() {
return array(
'name' => 'Update translations user interface',
'description' => 'Tests for the user interface of project interface translations.',
'group' => 'Localization Update',
);
}
function setUp() {
parent::setUp();
$admin_user = $this->drupalCreateUser(array('administer modules', 'administer site configuration', 'administer languages', 'access administration pages', 'translate interface'));
$this->drupalLogin($admin_user);
}
/**
* Tests the user interfaces of the interface translation update system.
*
* Testing the Available updates summary on the side wide status page and the
* Avaiable translation updates page.
*/
function testInterface() {
// Enable the module this test uses for its translations.
module_enable(array('l10n_update_test_translate'));
// No language added.
// Check status page and Available translation updates page.
$this->drupalGet('admin/reports/status');
$this->assertNoText(t('Translation update status'), 'No status message');
$this->drupalGet('admin/config/regional/translate/update');
$this->assertRaw(t('No translatable languages available. <a href="@add_language">Add a language</a> first.', array('@add_language' => url('admin/config/regional/language'))), 'Language message');
// Add German language.
$this->addLanguage('de');
// Drupal core is probably in 7.x, but tests may also be executed with
// stable releases. As this is an uncontrolled factor in the test, we will
// mark Drupal core as translated and continue with the prepared modules.
$status = l10n_update_get_status();
$status['drupal']['de']->type = 'current';
variable_set('l10n_update_translation_status', $status);
// One language added, all translations up to date.
$this->drupalGet('admin/reports/status');
$this->assertText(t('Translation update status'), 'Status message');
$this->assertText(t('Up to date'), 'Translations up to date');
$this->drupalGet('admin/config/regional/translate/update');
$this->assertText(t('All translations up to date.'), 'Translations up to date');
// Set l10n_update_test_translate module to have a local translation available.
$status = l10n_update_get_status();
$status['l10n_update_test_translate']['de']->type = 'local';
variable_set('l10n_update_translation_status', $status);
// Check if updates are available for German.
$this->drupalGet('admin/reports/status');
$this->assertText(t('Translation update status'), 'Status message');
$this->assertRaw(t('Updates available for: @languages. See the <a href="@updates">Available translation updates</a> page for more information.', array('@languages' => t('German'), '@updates' => url('admin/config/regional/translate/update'))), 'Updates available message');
$this->drupalGet('admin/config/regional/translate/update');
$this->assertText(t('Updates for: @modules', array('@modules' => 'Localization Update test translate')), 'Translations avaiable');
// Set l10n_update_test_translate module to have a dev release and no
// translation found.
$status = l10n_update_get_status();
$status['l10n_update_test_translate']['de']->version = '1.3-dev';
$status['l10n_update_test_translate']['de']->type = '';
variable_set('l10n_update_translation_status', $status);
// Check if no updates were found.
$this->drupalGet('admin/reports/status');
$this->assertText(t('Translation update status'), 'Status message');
$this->assertRaw(t('Missing translations for: @languages. See the <a href="@updates">Available translation updates</a> page for more information.', array('@languages' => t('German'), '@updates' => url('admin/config/regional/translate/update'))), 'Missing translations message');
$this->drupalGet('admin/config/regional/translate/update');
$this->assertText(t('Missing translations for one project'), 'No translations found');
$this->assertText(t('@module (@version).', array('@module' => 'Localization Update test translate', '@version' => '1.3-dev')), 'Release details');
$this->assertText(t('No translation files are provided for development releases.'), 'Release info');
}
}

View File

@@ -0,0 +1,440 @@
<?php
/**
* @file
* Contains L10nUpdateTest.
*/
/**
* Tests for update translations.
*/
class L10nUpdateTest extends L10nUpdateTestBase {
public static function getInfo() {
return array(
'name' => 'Update translations',
'description' => 'Tests for updating the interface translations of projects.',
'group' => 'Localization Update',
);
}
function setUp() {
parent::setUp();
$admin_user = $this->drupalCreateUser(array('administer modules', 'administer site configuration', 'administer languages', 'access administration pages', 'translate interface'));
$this->drupalLogin($admin_user);
// We use German as test language. This language must match the translation
// file that come with the l10n_update_test module (test.de.po) and can therefore
// not be chosen randomly.
$this->addLanguage('de');
module_load_include('compare.inc', 'l10n_update');
module_load_include('fetch.inc', 'l10n_update');
}
/**
* Checks if a list of translatable projects gets build.
*/
function testUpdateProjects() {
module_load_include('compare.inc', 'l10n_update');
variable_set('l10n_update_test_projects_alter', TRUE);
// Make the test modules look like a normal custom module. i.e. make the
// modules not hidden. l10n_update_test_system_info_alter() modifies the project
// info of the l10n_update_test and l10n_update_test_translate modules.
variable_set('l10n_update_test_system_info_alter', TRUE);
$this->resetAll();
// Check if interface translation data is collected from hook_info.
$projects = l10n_update_project_list();
$this->assertFalse(isset($projects['l10n_update_test_translate']), 'Hidden module not found');
$this->assertEqual($projects['l10n_update_test']['info']['interface translation server pattern'], 'sites/all/modules/l10n_update/tests/test.%language.po', 'Interface translation parameter found in project info.');
$this->assertEqual($projects['l10n_update_test']['name'] , 'l10n_update_test', format_string('%key found in project info.', array('%key' => 'interface translation project')));
}
/**
* Checks if local or remote translation sources are detected.
*
* The translation status process by default checks the status of the
* installed projects. For testing purpose a predefined set of modules with
* fixed file names and release versions is used. This custom project
* definition is applied using a hook_l10n_update_projects_alter
* implementation in the l10n_update_test module.
*
* This test generates a set of local and remote translation files in their
* respective local and remote translation directory. The test checks whether
* the most recent files are selected in the different check scenarios: check
* for local files only, check for both local and remote files.
*/
function testUpdateCheckStatus() {
// Set a flag to let the l10n_update_test module replace the project data with a
// set of test projects.
variable_set('l10n_update_test_projects_alter', TRUE);
// Create local and remote translations files.
$this->setTranslationFiles();
variable_set('l10n_update_default_filename', '%project-%release.%language._po');
// Set the test conditions.
$edit = array(
'l10n_update_check_mode' => L10N_UPDATE_USE_SOURCE_LOCAL,
);
$this->drupalPost('admin/config/regional/language/update', $edit, t('Save configuration'));
// Get status of translation sources at local file system.
$this->drupalGet('admin/config/regional/translate/check');
$result = l10n_update_get_status();
$this->assertEqual($result['contrib_module_one']['de']->type, L10N_UPDATE_LOCAL, 'Translation of contrib_module_one found');
$this->assertEqual($result['contrib_module_one']['de']->timestamp, $this->timestamp_old, 'Translation timestamp found');
$this->assertEqual($result['contrib_module_two']['de']->type, L10N_UPDATE_LOCAL, 'Translation of contrib_module_two found');
$this->assertEqual($result['contrib_module_two']['de']->timestamp, $this->timestamp_new, 'Translation timestamp found');
$this->assertEqual($result['l10n_update_test']['de']->type, L10N_UPDATE_LOCAL, 'Translation of l10n_update_test found');
$this->assertEqual($result['custom_module_one']['de']->type, L10N_UPDATE_LOCAL, 'Translation of custom_module_one found');
// Set the test conditions.
$edit = array(
'l10n_update_check_mode' => L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL,
);
$this->drupalPost('admin/config/regional/language/update', $edit, t('Save configuration'));
// Get status of translation sources at both local and remote locations.
$this->drupalGet('admin/config/regional/translate/check');
$result = l10n_update_get_status();
$this->assertEqual($result['contrib_module_one']['de']->type, L10N_UPDATE_REMOTE, 'Translation of contrib_module_one found');
$this->assertEqual($result['contrib_module_one']['de']->timestamp, $this->timestamp_new, 'Translation timestamp found');
$this->assertEqual($result['contrib_module_two']['de']->type, L10N_UPDATE_LOCAL, 'Translation of contrib_module_two found');
$this->assertEqual($result['contrib_module_two']['de']->timestamp, $this->timestamp_new, 'Translation timestamp found');
$this->assertEqual($result['contrib_module_three']['de']->type, L10N_UPDATE_LOCAL, 'Translation of contrib_module_three found');
$this->assertEqual($result['contrib_module_three']['de']->timestamp, $this->timestamp_old, 'Translation timestamp found');
$this->assertEqual($result['l10n_update_test']['de']->type, L10N_UPDATE_LOCAL, 'Translation of l10n_update_test found');
$this->assertEqual($result['custom_module_one']['de']->type, L10N_UPDATE_LOCAL, 'Translation of custom_module_one found');
}
/**
* Tests translation import from remote sources.
*
* Test conditions:
* - Source: remote and local files
* - Import overwrite: all existing translations
*/
function testUpdateImportSourceRemote() {
// Build the test environment.
$this->setTranslationFiles();
$this-> setCurrentTranslations();
variable_set('l10n_update_default_filename', '%project-%release.%language._po');
// Set the update conditions for this test.
$edit = array(
'l10n_update_check_mode' => L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL,
'overwrite' => LOCALE_IMPORT_OVERWRITE,
);
$this->drupalPost('admin/config/regional/language/update', $edit, t('Save configuration'));
// Get the translation status.
$this->drupalGet('admin/config/regional/translate/check');
// Check the status on the Available translation status page.
$this->assertRaw('<label class="element-invisible" for="edit-langcodes-de">Update German </label>', 'German language found');
$this->assertText('Updates for: Contributed module one, Contributed module two, Custom module one, Locale test', 'Updates found');
$this->assertText('Contributed module one (' . format_date($this->timestamp_new, 'medium') . ')', 'Updates for Contrib module one');
$this->assertText('Contributed module two (' . format_date($this->timestamp_new, 'medium') . ')', 'Updates for Contrib module two');
// Execute the translation update.
$this->drupalPost('admin/config/regional/translate/update', array(), t('Update translations'));
// Check if the translation has been updated, using the status cache.
$status = l10n_update_get_status();
$this->assertEqual($status['contrib_module_one']['de']->type, L10N_UPDATE_CURRENT, 'Translation of contrib_module_one found');
$this->assertEqual($status['contrib_module_two']['de']->type, L10N_UPDATE_CURRENT, 'Translation of contrib_module_two found');
$this->assertEqual($status['contrib_module_three']['de']->type, L10N_UPDATE_CURRENT, 'Translation of contrib_module_three found');
// Check the new translation status.
// The static cache needs to be flushed first to get the most recent data
// from the database. The function was called earlier during this test.
drupal_static_reset('l10n_update_get_file_history');
$history = l10n_update_get_file_history();
$this->assertTrue($history['contrib_module_one']['de']->timestamp >= $this->timestamp_now, 'Translation of contrib_module_one is imported');
$this->assertTrue($history['contrib_module_one']['de']->last_checked >= $this->timestamp_now, 'Translation of contrib_module_one is updated');
$this->assertEqual($history['contrib_module_two']['de']->timestamp, $this->timestamp_new, 'Translation of contrib_module_two is imported');
$this->assertTrue($history['contrib_module_two']['de']->last_checked >= $this->timestamp_now, 'Translation of contrib_module_two is updated');
$this->assertEqual($history['contrib_module_three']['de']->timestamp, $this->timestamp_medium, 'Translation of contrib_module_three is not imported');
$this->assertEqual($history['contrib_module_three']['de']->last_checked, $this->timestamp_medium, 'Translation of contrib_module_three is not updated');
// Check whether existing translations have (not) been overwritten.
$this->assertEqual(t('January', array(), array('langcode' => 'de')), 'Januar_1', 'Translation of January');
$this->assertEqual(t('February', array(), array('langcode' => 'de')), 'Februar_2', 'Translation of February');
$this->assertEqual(t('March', array(), array('langcode' => 'de')), 'Marz_2', 'Translation of March');
$this->assertEqual(t('April', array(), array('langcode' => 'de')), 'April_2', 'Translation of April');
$this->assertEqual(t('May', array(), array('langcode' => 'de')), 'Mai_customized', 'Translation of May');
$this->assertEqual(t('June', array(), array('langcode' => 'de')), 'Juni', 'Translation of June');
$this->assertEqual(t('Monday', array(), array('langcode' => 'de')), 'Montag', 'Translation of Monday');
}
/**
* Tests translation import from local sources.
*
* Test conditions:
* - Source: local files only
* - Import overwrite: all existing translations
*/
function testUpdateImportSourceLocal() {
// Build the test environment.
$this->setTranslationFiles();
$this-> setCurrentTranslations();
variable_set('l10n_update_default_filename', '%project-%release.%language._po');
// Set the update conditions for this test.
$edit = array(
'l10n_update_check_mode' => L10N_UPDATE_USE_SOURCE_LOCAL,
'overwrite' => LOCALE_IMPORT_OVERWRITE,
);
$this->drupalPost('admin/config/regional/language/update', $edit, t('Save configuration'));
// Execute the translation update.
$this->drupalGet('admin/config/regional/translate/check');
$this->drupalPost('admin/config/regional/translate/update', array(), t('Update translations'));
// Check if the translation has been updated, using the status cache.
$status = l10n_update_get_status();
$this->assertEqual($status['contrib_module_one']['de']->type, L10N_UPDATE_CURRENT, 'Translation of contrib_module_one found');
$this->assertEqual($status['contrib_module_two']['de']->type, L10N_UPDATE_CURRENT, 'Translation of contrib_module_two found');
$this->assertEqual($status['contrib_module_three']['de']->type, L10N_UPDATE_CURRENT, 'Translation of contrib_module_three found');
// Check the new translation status.
// The static cache needs to be flushed first to get the most recent data
// from the database. The function was called earlier during this test.
drupal_static_reset('l10n_update_get_file_history');
$history = l10n_update_get_file_history();
$this->assertTrue($history['contrib_module_one']['de']->timestamp >= $this->timestamp_medium, 'Translation of contrib_module_one is imported');
$this->assertEqual($history['contrib_module_one']['de']->last_checked, $this->timestamp_medium, 'Translation of contrib_module_one is updated');
$this->assertEqual($history['contrib_module_two']['de']->timestamp, $this->timestamp_new, 'Translation of contrib_module_two is imported');
$this->assertTrue($history['contrib_module_two']['de']->last_checked >= $this->timestamp_now, 'Translation of contrib_module_two is updated');
$this->assertEqual($history['contrib_module_three']['de']->timestamp, $this->timestamp_medium, 'Translation of contrib_module_three is not imported');
$this->assertEqual($history['contrib_module_three']['de']->last_checked, $this->timestamp_medium, 'Translation of contrib_module_three is not updated');
// Check whether existing translations have (not) been overwritten.
$this->assertEqual(t('January', array(), array('langcode' => 'de')), 'Januar_customized', 'Translation of January');
$this->assertEqual(t('February', array(), array('langcode' => 'de')), 'Februar_2', 'Translation of February');
$this->assertEqual(t('March', array(), array('langcode' => 'de')), 'Marz_2', 'Translation of March');
$this->assertEqual(t('April', array(), array('langcode' => 'de')), 'April_2', 'Translation of April');
$this->assertEqual(t('May', array(), array('langcode' => 'de')), 'Mai_customized', 'Translation of May');
$this->assertEqual(t('June', array(), array('langcode' => 'de')), 'Juni', 'Translation of June');
$this->assertEqual(t('Monday', array(), array('langcode' => 'de')), 'Montag', 'Translation of Monday');
}
/**
* Tests translation import and only overwrite non-customized translations.
*
* Test conditions:
* - Source: remote and local files
* - Import overwrite: only overwrite non-customized translations
*/
function testUpdateImportModeNonCustomized() {
// Build the test environment.
$this->setTranslationFiles();
$this-> setCurrentTranslations();
variable_set('l10n_update_default_filename', '%project-%release.%language._po');
// Set the test conditions.
$edit = array(
'l10n_update_check_mode' => L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL,
'overwrite' => L10N_UPDATE_OVERWRITE_NON_CUSTOMIZED,
);
$this->drupalPost('admin/config/regional/language/update', $edit, t('Save configuration'));
// Execute translation update.
$this->drupalGet('admin/config/regional/translate/check');
$this->drupalPost('admin/config/regional/translate/update', array(), t('Update translations'));
// Check whether existing translations have (not) been overwritten.
$this->assertEqual(t('January', array(), array('langcode' => 'de')), 'Januar_customized', 'Translation of January');
$this->assertEqual(t('February', array(), array('langcode' => 'de')), 'Februar_customized', 'Translation of February');
$this->assertEqual(t('March', array(), array('langcode' => 'de')), 'Marz_2', 'Translation of March');
$this->assertEqual(t('April', array(), array('langcode' => 'de')), 'April_2', 'Translation of April');
$this->assertEqual(t('May', array(), array('langcode' => 'de')), 'Mai_customized', 'Translation of May');
$this->assertEqual(t('June', array(), array('langcode' => 'de')), 'Juni', 'Translation of June');
$this->assertEqual(t('Monday', array(), array('langcode' => 'de')), 'Montag', 'Translation of Monday');
}
/**
* Tests translation import and don't overwrite any translation.
*
* Test conditions:
* - Source: remote and local files
* - Import overwrite: don't overwrite any existing translation
*/
function testUpdateImportModeNone() {
// Build the test environment.
$this->setTranslationFiles();
$this-> setCurrentTranslations();
variable_set('l10n_update_default_filename', '%project-%release.%language._po');
// Set the test conditions.
$edit = array(
'l10n_update_check_mode' => L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL,
'overwrite' => LOCALE_IMPORT_KEEP,
);
$this->drupalPost('admin/config/regional/language/update', $edit, t('Save configuration'));
// Execute translation update.
$this->drupalGet('admin/config/regional/translate/check');
$this->drupalPost('admin/config/regional/translate/update', array(), t('Update translations'));
// Check whether existing translations have (not) been overwritten.
$this->assertTranslation('January', 'Januar_customized', 'de');
$this->assertTranslation('February', 'Februar_customized', 'de');
$this->assertTranslation('March', 'Marz', 'de');
$this->assertTranslation('April', 'April_2', 'de');
$this->assertTranslation('May', 'Mai_customized', 'de');
$this->assertTranslation('June', 'Juni', 'de');
$this->assertTranslation('Monday', 'Montag', 'de');
}
/**
* Tests automatic translation import when a module is enabled.
*/
function testEnableUninstallModule() {
// Make the hidden test modules look like a normal custom module.
variable_set('l10n_update_test_system_info_alter', TRUE);
// Check if there is no translation yet.
$this->assertTranslation('Tuesday', '', 'de');
// Enable a module.
$edit = array(
'modules[Testing][l10n_update_test_translate][enable]' => '1',
);
$this->drupalPost('admin/modules', $edit, t('Save configuration'));
// Check if translations have been imported.
$this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.',
array('%number' => 0, '%update' => 7, '%delete' => 0)), 'One translation file imported.');
$this->assertTranslation('Tuesday', 'Dienstag', 'de');
// // Disable and uninstall a module
// module_disable(array('l10n_update_test_translate'));
// $edit = array(
// 'uninstall[l10n_update_test_translate]' => '1',
// );
// $this->drupalPost('admin/modules/uninstall', $edit, t('Uninstall'));
// $this->drupalPost(NULL, array(), t('Uninstall'));
//
// // Check if the file data is removed from the database.
// $history = l10n_update_get_file_history();
// $this->assertFalse(isset($history['l10n_update_test_translate']), 'Project removed from the file history');
// $projects = l10n_update_get_projects();
// $this->assertFalse(isset($projects['l10n_update_test_translate']), 'Project removed from the project list');
}
/**
* Tests automatic translation import when a langauge is enabled.
*
* When a language is added, the system will check for translations files of
* enabled modules and will import them. When a language is removed the system
* will remove all translations of that langugue from the database.
*/
function testEnableLanguage() {
// Make the hidden test modules look like a normal custom module.
variable_set('l10n_update_test_system_info_alter', TRUE);
// Enable a module.
$edit = array(
'modules[Testing][l10n_update_test_translate][enable]' => '1',
);
$this->drupalPost('admin/modules', $edit, t('Save configuration'));
// Check if there is no Dutch translation yet.
$this->assertTranslation('Extraday', '', 'nl');
$this->assertTranslation('Tuesday', 'Dienstag', 'de');
// Add a language.
$this->addLanguage('nl');
// Check if the right number of translations are added.
$this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.',
array('%number' => 0, '%update' => 8, '%delete' => 0)), 'One language added.');
$this->assertTranslation('Extraday', 'extra dag', 'nl');
// Check if the language data is added to the database.
$result = db_query("SELECT project FROM {l10n_update_file} WHERE language='nl'")->fetchField();
$this->assertTrue((boolean) $result, 'Files removed from file history');
// Remove a language.
$this->drupalPost('admin/config/regional/language/delete/nl', array(), t('Delete'));
// Check if the language data is removed from the database.
$result = db_query("SELECT project FROM {l10n_update_file} WHERE language='nl'")->fetchField();
$this->assertFalse($result, 'Files removed from file history');
// Check that the Dutch translation is gone.
$this->assertTranslation('Extraday', '', 'nl');
$this->assertTranslation('Tuesday', 'Dienstag', 'de');
}
/**
* Tests automatic translation import when a custom langauge is enabled.
*/
function testEnableCustomLanguage() {
// Make the hidden test modules look like a normal custom module.
variable_set('l10n_update_test_system_info_alter', TRUE);
// Enable a module.
$edit = array(
'modules[Testing][l10n_update_test_translate][enable]' => '1',
);
$this->drupalPost('admin/modules', $edit, t('Save configuration'));
// Create and enable a custom language with language code 'xx' and a random
// name.
$langcode = 'xx';
$name = $this->randomName(16);
$edit = array(
'langcode' => $langcode,
'name' => $name,
'native' => $name,
'prefix' => $langcode,
'direction' => '0',
);
$this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
drupal_static_reset('language_list');
$languages = language_list();
$this->assertTrue(isset($languages[$langcode]), format_string('Language %langcode added.', array('%langcode' => $langcode)));
// Ensure the translation file is automatically imported when the language
// was added.
$this->assertText(t('One translation file imported.'), 'Language file automatically imported.');
$this->assertText(t('One translation string was skipped because of disallowed or malformed HTML'), 'Language file automatically imported.');
// Ensure the strings were successfully imported.
$search = array(
'string' => 'lundi',
'language' => $langcode,
'translation' => 'translated',
);
$this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
$this->assertNoText(t('No strings available.'), 'String successfully imported.');
// Ensure the multiline string was imported.
$search = array(
'string' => 'Source string for multiline translation',
'language' => $langcode,
'translation' => 'all',
);
$this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
$this->assertText('Source string for multiline translation', 'String successfully imported.');
// Ensure 'Allowed HTML source string' was imported but the translation for
// 'Another allowed HTML source string' was not because it contains invalid
// HTML.
$search = array(
'string' => 'HTML source string',
'language' => $langcode,
'translation' => 'translated',
);
$this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
// $this->assertText('Allowed HTML source string', 'String successfully imported.');
$this->assertNoText('Another allowed HTML source string', 'String with disallowed translation not imported.');
}
}

View File

@@ -0,0 +1,284 @@
<?php
/**
* @file
* Contains L10nUpdateTest.
*/
/**
* Tests for update translations.
*/
class L10nUpdateTestBase extends DrupalWebTestCase {
/**
* Timestamp for an old translation.
*
* @var integer
*/
protected $timestamp_old;
/**
* Timestamp for a medium aged translation.
*
* @var integer
*/
protected $timestamp_medium;
/**
* Timestamp for a new translation.
*
* @var integer
*/
protected $timestamp_new;
function setUp() {
parent::setUp('update', 'locale', 'l10n_update', 'l10n_update_test');
// Setup timestamps to identify old and new translation sources.
$this->timestamp_old = REQUEST_TIME - 300;
$this->timestamp_medium = REQUEST_TIME - 200;
$this->timestamp_new = REQUEST_TIME - 100;
$this->timestamp_now = REQUEST_TIME;
}
/**
* Sets the value of the default translations directory.
*
* @param string $path
* Path of the translations directory relative to the drupal installation
* directory.
*/
protected function setTranslationsDirectory($path) {
file_prepare_directory($path, FILE_CREATE_DIRECTORY);
variable_set('l10n_update_download_store', $path);
}
/**
* Adds a language.
*
* @param $langcode
* The language code of the language to add.
*/
protected function addLanguage($langcode) {
$edit = array('langcode' => $langcode);
$this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
drupal_static_reset('language_list');
$languages = language_list();
$this->assertTrue(isset($languages[$langcode]), format_string('Language %langcode added.', array('%langcode' => $langcode)));
}
/**
* Creates a translation file and tests its timestamp.
*
* @param string $path
* Path of the file relative to the public file path.
* @param string $filename
* Name of the file to create.
* @param integer $timestamp
* Timestamp to set the file to. Defaults to current time.
* @param array $translations
* Array of source/target value translation strings. Only singular strings
* are supported, no plurals. No double quotes are allowed in source and
* translations strings.
*/
protected function makePoFile($path, $filename, $timestamp = NULL, $translations = array()) {
$timestamp = $timestamp ? $timestamp : REQUEST_TIME;
$path = 'public://' . $path;
$text = '';
$po_header = <<<EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 7\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
EOF;
// Convert array of translations to Gettext source and translation strings.
if ($translations) {
foreach ($translations as $source => $target) {
$text .= 'msgid "'. $source . '"' . "\n";
$text .= 'msgstr "'. $target . '"' . "\n";
}
}
file_prepare_directory($path, FILE_CREATE_DIRECTORY);
$file = (object) array(
'uid' => 1,
'filename' => $filename,
'uri' => $path . '/' . $filename,
'filemime' => 'text/x-gettext-translation',
'timestamp' => $timestamp,
'status' => FILE_STATUS_PERMANENT,
);
file_put_contents($file->uri, $po_header . $text);
touch(drupal_realpath($file->uri), $timestamp);
file_save($file);
}
/**
* Setup the environment containing local and remote translation files.
*
* Update tests require a simulated environment for local and remote files.
* Normally remote files are located at a remote server (e.g. ftp.drupal.org).
* For testing we can not rely on this. A directory in the file system of the
* test site is designated for remote files and is addressed using an absolute
* URL. Because Drupal does not allow files with a po extension to be accessed
* (denied in .htaccess) the translation files get a _po extension. Another
* directory is designated for local translation files.
*
* The environment is set up with the following files. File creation times are
* set to create different variations in test conditions.
* contrib_module_one
* - remote file: timestamp new
* - local file: timestamp old
* - current: timestamp medium
* contrib_module_two
* - remote file: timestamp old
* - local file: timestamp new
* - current: timestamp medium
* contrib_module_three
* - remote file: timestamp old
* - local file: timestamp old
* - current: timestamp medium
* custom_module_one
* - local file: timestamp new
* - current: timestamp medium
* Time stamp of current translation set by setCurrentTranslations() is always
* timestamp medium. This makes it easy to predict which translation will be
* imported.
*/
protected function setTranslationFiles() {
// A flag is set to let the l10n_update_test module replace the project data with
// a set of test projects which match the below project files.
variable_set('l10n_update_test_projects_alter', TRUE);
// Setup the environment.
$public_path = drupal_realpath('public://');
$this->setTranslationsDirectory($public_path . '/local');
variable_set('l10n_update_default_filename', '%project-%release.%language._po');
// Setting up sets of translations for the translation files.
$translations_one = array('January' => 'Januar_1', 'February' => 'Februar_1', 'March' => 'Marz_1');
$translations_two = array( 'February' => 'Februar_2', 'March' => 'Marz_2', 'April' => 'April_2');
$translations_three = array('April' => 'April_3', 'May' => 'Mai_3', 'June' => 'Juni_3');
// Add a number of files to the local file system to serve as remote
// translation server and match the project definitions set in
// l10n_update_test_l10n_update_projects_alter().
$this->makePoFile('remote/7.x/contrib_module_one', 'contrib_module_one-7.x-1.1.de._po', $this->timestamp_new, $translations_one);
$this->makePoFile('remote/7.x/contrib_module_two', 'contrib_module_two-7.x-2.0-beta4.de._po', $this->timestamp_old, $translations_two);
$this->makePoFile('remote/7.x/contrib_module_three', 'contrib_module_three-7.x-1.0.de._po', $this->timestamp_old, $translations_three);
// Add a number of files to the local file system to serve as local
// translation files and match the project definitions set in
// l10n_update_test_l10n_update_projects_alter().
$this->makePoFile('local', 'contrib_module_one-7.x-1.1.de._po', $this->timestamp_old, $translations_one);
$this->makePoFile('local', 'contrib_module_two-7.x-2.0-beta4.de._po', $this->timestamp_new, $translations_two);
$this->makePoFile('local', 'contrib_module_three-7.x-1.0.de._po', $this->timestamp_old, $translations_three);
$this->makePoFile('local', 'custom_module_one.de.po', $this->timestamp_new);
}
/**
* Setup existing translations in the database and set up the status of
* existing translations.
*/
protected function setCurrentTranslations() {
// Setup to add German translations to the database.
$langcode = 'de';
$writer = new PoDatabaseWriter();
$writer->setLangcode($langcode);
$writer->setOptions(array(
'overwrite_options' => array(
'not_customized' => TRUE,
'customized' => TRUE,
),
));
// Add non customized translations to the database.
$writer->setOptions(array('customized' => L10N_UPDATE_NOT_CUSTOMIZED));
$non_customized_translations = array(
'March' => 'Marz',
'June' => 'Juni',
);
foreach ($non_customized_translations as $source => $translation) {
$poItem = new PoItem();
$poItem->setFromArray(array(
'source' => $source,
'translation' => $translation,
));
$writer->writeItem($poItem);
}
// Add customized translations to the database.
$writer->setOptions(array('customized' => L10N_UPDATE_CUSTOMIZED));
$customized_translations = array(
'January' => 'Januar_customized',
'February' => 'Februar_customized',
'May' => 'Mai_customized',
);
foreach ($customized_translations as $source => $translation) {
$poItem = new PoItem();
$poItem->setFromArray(array(
'source' => $source,
'translation' => $translation,
));
$writer->writeItem($poItem);
}
// Add a state of current translations in l10n_update_files.
$default = array(
'language' => $langcode,
'uri' => '',
'timestamp' => $this->timestamp_medium,
'last_checked' => $this->timestamp_medium,
);
$data[] = array(
'project' => 'contrib_module_one',
'filename' => 'contrib_module_one-7.x-1.1.de._po',
'version' => '7.x-1.1',
);
$data[] = array(
'project' => 'contrib_module_two',
'filename' => 'contrib_module_two-7.x-2.0-beta4.de._po',
'version' => '7.x-2.0-beta4',
);
$data[] = array(
'project' => 'contrib_module_three',
'filename' => 'contrib_module_three-7.x-1.0.de._po',
'version' => '7.x-1.0',
);
$data[] = array(
'project' => 'custom_module_one',
'filename' => 'custom_module_one.de.po',
'version' => '',
);
foreach ($data as $file) {
$file = (object) array_merge($default, $file);
drupal_write_record('l10n_update_file', $file);
}
}
/**
* Checks the translation of a string.
*
* @param string $source
* Translation source string
* @param string $translation
* Translation to check. Use empty string to check for a not existing
* translation.
* @param string $langcode
* Language code of the language to translate to.
* @param string $message
* (optional) A message to display with the assertion.
*/
protected function assertTranslation($source, $translation, $langcode, $message = '') {
$db_translation = db_query('SELECT translation FROM {locales_target} lt INNER JOIN {locales_source} ls ON ls.lid = lt.lid WHERE ls.source = :source AND lt.language = :langcode', array(':source' => $source, ':langcode' => $langcode))->fetchField();
$db_translation = $db_translation == FALSE ? '' : $db_translation;
$this->assertEqual($translation, $db_translation, $message ? $message : format_string('Correct translation of %source (%language)', array('%source' => $source, '%language' => $langcode)));
}
}

View File

@@ -0,0 +1,14 @@
name = 'Localization Update test'
type = module
description = 'Support module for Localization Update module testing.'
package = Testing
version = '1.2'
core = 7.x
hidden = true
; Information added by Drupal.org packaging script on 2014-11-10
version = "7.x-2.0"
core = "7.x"
project = "l10n_update"
datestamp = "1415625781"

View File

@@ -0,0 +1,15 @@
<?php
/**
* @file
* Install, update and uninstall functions for the l10n_update_test module.
*/
/**
* Implements hook_uninstall().
*/
function l10n_update_test_uninstall() {
// Clear variables.
variable_del('l10n_update_test_system_info_alter');
variable_del('l10n_update_test_projects_alter');
}

View File

@@ -0,0 +1,143 @@
<?php
/**
* @file
* Simulate a custom module with a local po file.
*/
/**
* Implements hook_system_info_alter().
*
* Make the test scripts to be believe this is not a hidden test module, but
* a regular custom module.
*/
function l10n_update_test_system_info_alter(&$info, $file, $type) {
// Only modify the system info if required.
// By default the l10n_update_test modules are hidden and have a project specified.
// To test the module detection process by l10n_update_project_list() the
// test modules should mimic a custom module. I.e. be non-hidden.
if (variable_get('l10n_update_test_system_info_alter', FALSE)) {
if ($file->name == 'l10n_update_test' || $file->name == 'l10n_update_test_translate') {
// Don't hide the module.
$info['hidden'] = FALSE;
}
}
}
/**
* Implements hook_l10n_update_projects_alter().
*
* The translation status process by default checks the status of the installed
* projects. This function replaces the data of the installed modules by a
* predefined set of modules with fixed file names and release versions. Project
* names, versions, timestamps etc must be fixed because they must match the
* files created by the test script.
*
* The "l10n_update_test_projects_alter" variable must be set by the test script
* in order for this hook to take effect.
*/
function l10n_update_test_l10n_update_projects_alter(&$projects) {
if (variable_get('l10n_update_test_projects_alter', FALSE)) {
// Instead of the default ftp.drupal.org we use the file system of the test
// instance to simulate a remote file location.
$wrapper = file_stream_wrapper_get_instance_by_uri('public://');
$remote_url = $wrapper->getExternalUrl() . '/remote/';
// Completely replace the project data with a set of test projects.
$projects = array (
'contrib_module_one' => array (
'name' => 'contrib_module_one',
'info' => array (
'name' => 'Contributed module one',
'l10n path' => $remote_url . '%core/%project/%project-%release.%language._po',
'package' => 'Other',
'version' => '7.x-1.1',
'project' => 'contrib_module_one',
'datestamp' => '1344471537',
'_info_file_ctime' => 1348767306,
),
'datestamp' => '1344471537',
'includes' => array (
'contrib_module_one' => 'Contributed module one',
),
'project_type' => 'module',
'project_status' => TRUE,
),
'contrib_module_two' => array (
'name' => 'contrib_module_two',
'info' => array (
'name' => 'Contributed module two',
'l10n path' => $remote_url . '%core/%project/%project-%release.%language._po',
'package' => 'Other',
'version' => '7.x-2.0-beta4',
'project' => 'contrib_module_two',
'datestamp' => '1344471537',
'_info_file_ctime' => 1348767306,
),
'datestamp' => '1344471537',
'includes' => array (
'contrib_module_two' => 'Contributed module two',
),
'project_type' => 'module',
'project_status' => TRUE,
),
'contrib_module_three' => array (
'name' => 'contrib_module_three',
'info' => array (
'name' => 'Contributed module three',
'l10n path' => $remote_url . '%core/%project/%project-%release.%language._po',
'package' => 'Other',
'version' => '7.x-1.0',
'project' => 'contrib_module_three',
'datestamp' => '1344471537',
'_info_file_ctime' => 1348767306,
),
'datestamp' => '1344471537',
'includes' => array (
'contrib_module_three' => 'Contributed module three',
),
'project_type' => 'module',
'project_status' => TRUE,
),
'l10n_update_test' => array (
'name' => 'l10n_update_test',
'info' => array (
'name' => 'Locale test',
'interface translation project' => 'l10n_update_test',
'l10n path' => 'sites/all/modules/l10n_update/tests/test.%language.po',
'package' => 'Other',
'version' => NULL,
'project' => 'l10n_update_test',
'_info_file_ctime' => 1348767306,
'datestamp' => 0,
),
'datestamp' => 0,
'includes' => array (
'l10n_update_test' => 'Locale test',
),
'project_type' => 'module',
'project_status' => TRUE,
),
'custom_module_one' => array (
'name' => 'custom_module_one',
'info' => array (
'name' => 'Custom module one',
'interface translation project' => 'custom_module_one',
'l10n path' => 'translations://custom_module_one.%language.po',
'package' => 'Other',
'version' => NULL,
'project' => 'custom_module_one',
'_info_file_ctime' => 1348767306,
'datestamp' => 0,
),
'datestamp' => 0,
'includes' => array (
'custom_module_one' => 'Custom module one',
),
'project_type' => 'module',
'project_status' => TRUE,
),
);
}
}

View File

@@ -0,0 +1,16 @@
name = 'Localization Update test translate'
type = module
description = 'Translation test module for Localization Update module testing.'
package = Testing
version = '1.3'
core = 7.x
hidden = true
interface translation project = l10n_update_test_translate
l10n path = sites/all/modules/contrib/l10n_update/tests/modules/l10n_update_test_translate/translations/l10n_update_test_translate.%language.po
; Information added by Drupal.org packaging script on 2014-11-10
version = "7.x-2.0"
core = "7.x"
project = "l10n_update"
datestamp = "1415625781"

View File

@@ -0,0 +1,28 @@
<?php
/**
* @file
* Simulates a custom module with a local po file.
*/
/**
* Implements hook_system_info_alter().
*
* By default this modules is hidden but once enabled it behaves like a normal
* (not hidden) module. This hook implementation changes the .info.yml data by
* setting the hidden status to FALSE.
*/
function l10n_update_test_translate_system_info_alter(&$info, $file, $type) {
if ($file->name == 'l10n_update_test_translate') {
// Don't hide the module.
if (isset($info['hidden'])) {
$info['hidden'] = FALSE;
}
// Correct the path to the translation file. At a test-environment, the
// module may be place in a different path.
$basename = basename($info['l10n path']);
$path = drupal_get_path('module', 'l10n_update') . '/tests/modules/l10n_update_test_translate/translations/';
$info['l10n path'] = $path . $basename;
}
}

View File

@@ -0,0 +1,28 @@
msgid ""
msgstr ""
"Project-Id-Version: Drupal 7\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
msgid "Monday"
msgstr "Montag"
msgid "Tuesday"
msgstr "Dienstag"
msgid "Wednesday"
msgstr "Mittwoch"
msgid "Thursday"
msgstr "Donnerstag"
msgid "Friday"
msgstr "Freitag"
msgid "Saturday"
msgstr "Samstag"
msgid "Sunday"
msgstr "Sonntag"

View File

@@ -0,0 +1,31 @@
msgid ""
msgstr ""
"Project-Id-Version: Drupal 7\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
msgid "Monday"
msgstr "maandag"
msgid "Tuesday"
msgstr "dinsdag"
msgid "Wednesday"
msgstr "woensdag"
msgid "Thursday"
msgstr "donderdag"
msgid "Extraday"
msgstr "extra dag"
msgid "Friday"
msgstr "vrijdag"
msgid "Saturday"
msgstr "zaterdag"
msgid "Sunday"
msgstr "zondag"

View File

@@ -0,0 +1,40 @@
msgid ""
msgstr ""
"Project-Id-Version: Drupal 7\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
msgid "Monday"
msgstr "lundi"
msgid "Tuesday"
msgstr "mardi"
msgid "Wednesday"
msgstr "mercredi"
msgid "Thursday"
msgstr "jeudi"
msgid "Friday"
msgstr "vendredi"
msgid "Saturday"
msgstr "samedi"
msgid "Sunday"
msgstr "dimanche"
msgid "Allowed HTML source string"
msgstr "<strong>Allowed HTML translation string</strong>"
msgid "Another allowed HTML source string"
msgstr "<script>Disallowed HTML translation string</script>"
msgid "Source string for multiline translation"
msgstr ""
"Multiline translation string "
"to make sure that "
"import works with it."

View File

@@ -0,0 +1,10 @@
msgid ""
msgstr ""
"Project-Id-Version: Drupal 7\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
msgid "Monday"
msgstr "Montag"