diff options
Diffstat (limited to 'includes')
-rw-r--r-- | includes/common.inc | 38 | ||||
-rw-r--r-- | includes/conf.php | 9 | ||||
-rw-r--r-- | includes/locale.inc | 1268 |
3 files changed, 1299 insertions, 16 deletions
diff --git a/includes/common.inc b/includes/common.inc index 209769c8f..d5eb1d1f3 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -505,8 +505,15 @@ function message_na() { /** * Initialize the localization system. */ -function locale_init() { - global $languages, $user; +function locale_initialize() { + global $user; + if (function_exists('locale')) { + $languages = locale_supported_languages(); + $languages = $languages['name']; + } + else { + $languages = array(); + } if ($user->uid && $languages[$user->language]) { return $user->language; } @@ -540,9 +547,10 @@ function locale_init() { * The translated string. */ function t($string, $args = 0) { - global $languages; - - $string = ($languages && module_exist('locale') ? locale($string) : $string); + global $locale; + if (function_exists('locale') && $locale != 'en') { + $string = locale($string); + } if (!$args) { return $string; @@ -841,7 +849,23 @@ function format_rss_item($title, $link, $description, $args = array()) { * A translated string. */ function format_plural($count, $singular, $plural) { - return t($count == 1 ? $singular : $plural, array('%count' => $count)); + if ($count == 1) return t($singular); + + // get the plural index through the gettext formula + $index = (function_exists('locale')) ? locale_get_plural($count) : -1; + if ($index < 0) { // backward compatibility + return t($plural, array("%count" => $count)); + } + else { + switch ($index) { + case "0": + return t($singular); + case "1": + return t($plural, array("%count" => $count)); + default: + return t(strtr($plural, array("%count" => '%count['. $index .']')), array('%count['. $index .']' => $count)); + } + } } /** @@ -1814,7 +1838,7 @@ if ($_REQUEST && !user_access('bypass input data check')) { } // initialize localization system: -$locale = locale_init(); +$locale = locale_initialize(); // initialize theme: $theme = init_theme(); diff --git a/includes/conf.php b/includes/conf.php index cf72381f8..0c78ed344 100644 --- a/includes/conf.php +++ b/includes/conf.php @@ -46,15 +46,6 @@ $base_url = "http://localhost"; // ini_set("include_path", ".:/path/to/pear"); # -# Languages / translation / internationalization: -# -# The first language listed in this associative array will -# automatically become the default language. You can add a language -# but make sure your SQL table, called locales is updated -# appropriately. -$languages = array("en" => "english"); - -# # Custom navigation links: # # Custom navigation links override the standard page links offered diff --git a/includes/locale.inc b/includes/locale.inc new file mode 100644 index 000000000..5d6ceb18c --- /dev/null +++ b/includes/locale.inc @@ -0,0 +1,1268 @@ +<?php +// $Id$ +/** + * @file + * + * Admin related functions for locale.module + * + */ + +// --------------------------------------------------------------------------------- +// Language addition functionality (administration only) + +/** + * Helper function to add a language + */ +function _locale_add_language($code, $name, $onlylanguage = TRUE) { + db_query("INSERT INTO {locales_meta} (locale, name) VALUES ('%s','%s')", $code, $name); + $result = db_query("SELECT lid FROM {locales_source}"); + while ($string = db_fetch_object($result)) { + db_query("INSERT INTO {locales_target} (lid, locale) VALUES (%d,'%s')", $string->lid, $code); + } + + // If only the language was added, and not a PO file import triggered + // the language addition, we need to inform the user on how to start + // a translation + if ($onlylanguage) { + $message = t("'%locale' language added. You can now import a translation. See the <a href=\"%locale-help\">help screen</a> for more information.", array('%locale' => t($name), '%locale-help' => url("admin/help/locale"))); + } + else { + $message = t("'%locale' language added.", array('%locale' => t($name))); + } + + drupal_set_message($message); + watchdog('locale', t("'%locale' language added.", array('%locale' => $code))); +} + +/** + * User interface for the language management screen + */ +function _locale_admin_manage_screen() { + $edit = &$_POST['edit']; + $languages = locale_supported_languages(TRUE, TRUE); + + $header = array(array('data' => t('code')), array('data' => t('English name')), array('data' => t('enabled')), array('data' => t('default')), array('data' => t('translated')), array('data' => t('operations'))); + + foreach ($languages['name'] as $key => $lang) { + + $status = db_fetch_object(db_query("SELECT isdefault, enabled FROM {locales_meta} WHERE locale = '%s'", $key)); + + if ($key == 'en') { + $rows[] = array('en', $lang, form_checkbox('', 'enabled][en', 1, $status->enabled), form_radio('', 'sitedefault', $key, $status->isdefault), message_na(), ''); + } + else { + $original = db_fetch_object(db_query("SELECT COUNT(*) AS strings FROM {locales_source}")); + $translation = db_fetch_object(db_query("SELECT COUNT(*) AS translation FROM {locales_target} WHERE locale = '%s' AND translation != ''", $key)); + + $ratio = ($original->strings > 0 && $translation->translation > 0) ? round(($translation->translation/$original->strings)*100., 2) : 0; + + $rows[] = array($key, ($key != 'en' ? form_textfield('', 'name]['. $key, $lang, 15, 64) : $lang), form_checkbox('', 'enabled]['. $key, 1, $status->enabled), form_radio('', 'sitedefault', $key, $status->isdefault), "$translation->translation/$original->strings ($ratio%)", ($key != 'en' ? l(t('delete locale'), 'admin/locale/language/delete/'. urlencode($key)) : '')); + } + } + + return form(theme('table', $header, $rows) . form_submit(t('Save configuration')), 'POST', url('admin/locale')); +} + +/** + * User interface for the language addition screen + */ +function _locale_admin_manage_add_screen() { + + $isocodes = _locale_prepare_iso_list(); + + $output = '<h2>'. t('From language list') .'</h2>'; + $form = form_select(t('Language name'), 'langcode', key($isocodes), $isocodes, t('Select your language here, or add it below, if you are unable to find it.')); + $form .= form_submit(t('Add language')); + $output .= form($form); + + $edit = &$_POST['edit']; + $output .= '<h2>'. t('Custom language') .'</h2>'; + $form = form_textfield(t('Language code'), 'langcode', $edit['langcode'], 70, 12, t("Commonly this is an <a href=\"%iso-codes\">ISO 639 language code</a> with an optional country code for regional variants. Examples include 'en', 'en-US' and 'zh-cn'.", array("%iso-codes" => "http://www.w3.org/WAI/ER/IG/ert/iso639.htm"))); + $form .= form_textfield(t('Language name in English'), 'langname', $edit['langname'], 70, 64, t('Name of the language. Will be availabale for translation in all languages.')); + $form .= form_submit(t('Add language')); + $output .= form($form); + + return $output; +} + + +/** + * User interface for the translation import screen + */ +function _locale_admin_import_screen() { + $languages = locale_supported_languages(FALSE, TRUE); + $languages = array_map("t", $languages['name']); + unset($languages['en']); + + if (!count($languages)) { + drupal_set_message(t('You need to have at least one language set up to import translations.'), 'error'); + } + else { + $languages = array( + t('Already added languages') => $languages, + t('Languages not yet added') => _locale_prepare_iso_list() + ); + + $form = form_file(t('Language file'), 'file', 50, t('A gettext Portable Object (.po) file.')); + $form .= form_select(t('Import into'), 'langcode', '', $languages, t('Choose the language you want to add strings into. If you choose a language which is not yet set up, then it will be added.')); + $form .= form_radios(t('Mode'), 'mode', 'overwrite', array("overwrite" => t('Strings in the uploaded file replace existing ones, new ones are added'), "keep" => t('Existing strings are kept, only new strings are added'))); + $form .= form_submit(t('Import')); + $output = form($form, 'POST', url('admin/locale/language/import'), array('enctype' => 'multipart/form-data')); + } + return $output; +} + +/** + * Parses Gettext Portable Object file information and inserts into database + * + * @param $file Name of local file to be imported + * @param $edit Language code + * @param $mode should existing translations be replaced? + */ +function _locale_import_po($file, $lang, $mode) { + // Check if we have the language already in the database + if (!db_fetch_object(db_query("SELECT locale FROM {locales_meta} WHERE locale = '%s'", $lang))) { + drupal_set_message(t("Unsupported language selected for import."), 'error'); + return FALSE; + } + + // Check if we can get the strings from the file + if (!($strings = _locale_import_read_po($file))) { + drupal_set_message(t("Translation file broken: Couldn't be read."), 'error'); + return FALSE; + } + + // Strip out header from the string pairs + $header = $strings[""]["msgstr"]; + unset($strings[""]); + + // Get information from the header into the database + if ($header) { + $hdr = _locale_import_parse_header($header); + + // Get the plural formula + if ($hdr["Plural-Forms"] && $p = _locale_import_parse_plural_forms($hdr["Plural-Forms"])) { + list($nplurals, $plural) = $p; + db_query("UPDATE {locales_meta} SET plurals = '%d', formula = '%s' WHERE locale = '%s'", $nplurals, $plural, $lang); + } + else { + db_query("UPDATE {locales_meta} SET plurals = '%d', formula = '%s' WHERE locale = '%s'", 0, '', $lang); + } + } + else { + drupal_set_message(t("Translation file broken: No header."), 'error'); + return FALSE; + } + + $fullstr = 0; + foreach ($strings as $value) { + $comments = _locale_import_shorten_comments($value['#']); + + // Handle a translation for some plural string + if (strpos($value['msgid'], "\0")) { + $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 ($trans != '') { + $fullstr++; + } + $loc = db_fetch_object(db_query("SELECT s.lid, t.translation FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid WHERE s.source = '%s' AND t.locale = '%s'", $english[$key], $lang)); + if ($loc->lid) { + $lid = $loc->lid; + db_query("UPDATE {locales_source} SET location = '%s' WHERE lid = %d", $comments, $lid); + } + else { + db_query("INSERT INTO {locales_source} (location, source) VALUES ('%s', '%s')", $comments, $english[$key]); + $lid = db_fetch_object(db_query("SELECT lid FROM {locales_source} WHERE location = '%s' AND source = '%s'", $comments, $english[$key])); + $lid = $lid->lid; + } + if ($key == 0) { + $parent = $lid; + } + if ($loc->translation && $mode == 'overwrite') { + db_query("UPDATE {locales_target} SET translation = '%s', plid = %d, plural = %d WHERE locale = '%s' AND lid = %d", $trans, $parent, $key, $lang, $lid); + } + elseif (!$loc->translation) { + db_query("INSERT INTO {locales_target} (lid, locale, translation, plid, plural) VALUES (%d, '%s', '%s', %d, %d)", $lid, $lang, $trans, $parent, $key); + } + } + } + + // A simple translation + else { + $english = $value['msgid']; + $translation = $value['msgstr']; + if ($translation != '') { + $fullstr++; + } + $loc = db_fetch_object(db_query("SELECT s.lid, t.translation FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid WHERE s.source = '%s' AND t.locale = '%s'", $english, $lang)); + if ($loc->lid) { + $lid = $loc->lid; + db_query("UPDATE {locales_source} SET location = '%s' WHERE source = '%s'", $comments, $english); + } + else { + db_query("INSERT INTO {locales_source} (location, source) VALUES ('%s', '%s')", $comments, $english); + $loc = db_fetch_object(db_query("SELECT lid FROM {locales_source} WHERE location = '%s' AND source = '%s'", $comments, $english)); + $lid = $loc->lid; + } + if ($loc->translation && $mode == 'overwrite') { + db_query("UPDATE {locales_target} SET translation = '%s' WHERE locale = '%s' AND lid = %d", $translation, $lang, $lid); + } + elseif (!$loc->translation) { + db_query("INSERT INTO {locales_target} (lid, locale, translation) VALUES (%d, '%s', '%s')", $lid, $lang, $translation); + } + } + } + + // Successfull import + cache_clear_all("locale:$lang"); + drupal_set_message(t("Translation successfully imported. %num translated strings added to language.", array('%num' => $fullstr))); + watchdog('locale', strtr("Translation imported into '%locale', %num translated strings added to language.", array('%locale' => $lang, '%num' => $fullstr))); + return TRUE; +} + +/** + * Parses Gettext Portable Object file into an array + * + * @param $path Name of local file to parse + * @author Jacobo Tarrio + */ +function _locale_import_read_po($path) { + + $fd = fopen($path, "rb"); + if (!$fd) { + drupal_set_message(t("Translation import failed: File '%path' cannot be read.", array("%path" => $path)), 'error'); + return FALSE; + } + $info = fstat($fd); + $len = $info["size"]; + $po = fread($fd, $len); + fclose($fd); + + $context = "COMMENT"; // Parser context: COMMENT, MSGID, MSGID_PLURAL, MSGSTR and MSGSTR_ARR + $current = array(); // Current entry being read + $strings = array(); // List of entries read + $plural = 0; // Current plural form + + $po = strtr($po, array("\\\n" => "")); + $lines = split("\n", $po); + $lineno = 0; + + foreach ($lines as $line) { + $lineno++; + $line = trim($line); + + 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 + $strings[$current["msgid"]] = $current; + $current = array(); + $current["#"][] = substr($line, 1); + $context = "COMMENT"; + } + else { // Parse error + drupal_set_message(t("Translation file broken: Expected \"msgstr\" in line %lineno", array("%lineno" => $lineno)), 'error'); + return FALSE; + } + } + elseif (!strncmp("msgid_plural", $line, 12)) { + if ($context != "MSGID") { // Must be plural form for current entry + drupal_set_message(t("Translation file broken: Unexpected \"msgid_plural\" in line %lineno", array("%lineno" => $lineno)), 'error'); + return FALSE; + } + $line = trim(substr($line, 12)); + $quoted = _locale_import_parse_quoted($line); + if ($quoted === false) { + drupal_set_message(t("Translation file broken: Syntax error in line %lineno", array("%lineno" => $lineno)), 'error'); + return FALSE; + } + $current["msgid"] = $current["msgid"] ."\0". $quoted; + $context = "MSGID_PLURAL"; + } + elseif (!strncmp("msgid", $line, 5)) { + if ($context == "MSGSTR") { // End current entry, start a new one + $strings[$current["msgid"]] = $current; + $current = array(); + } + elseif ($context == "MSGID") { // Already in this context? Parse error + drupal_set_message(t("Translation file broken: Unexpected \"msgid\" in line %lineno", array("%lineno" => $lineno)), 'error'); + return FALSE; + } + $line = trim(substr($line, 5)); + $quoted = _locale_import_parse_quoted($line); + if ($quoted === false) { + drupal_set_message(t("Translation file broken: Syntax error in line %lineno", array("%lineno" => $lineno)), 'error'); + return FALSE; + } + $current["msgid"] = $quoted; + $context = "MSGID"; + } + elseif (!strncmp("msgstr[", $line, 7)) { + if (($context != "MSGID") && ($context != "MSGID_PLURAL") && ($context != "MSGSTR_ARR")) { // Must come after msgid, msgid_plural, or msgstr[] + drupal_set_message(t("Translation file broken: Unexpected \"msgstr[]\" in line %lineno", array("%lineno" => $lineno)), 'error'); + return FALSE; + } + if (strpos($line, "]") === false) { + drupal_set_message(t("Translation file broken: Syntax error in line %lineno", array("%lineno" => $lineno)), 'error'); + 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) { + drupal_set_message(t("Translation file broken: Syntax error in line %lineno", array("%lineno" => $lineno)), 'error'); + return FALSE; + } + $current["msgstr"][$plural] = $quoted; + $context = "MSGSTR_ARR"; + } + elseif (!strncmp("msgstr", $line, 6)) { + if ($context != "MSGID") { // Should come just after a msgid block + drupal_set_message(t("Translation file broken: Unexpected \"msgstr\" in line %lineno", array("%lineno" => $lineno)), 'error'); + return FALSE; + } + $line = trim(substr($line, 6)); + $quoted = _locale_import_parse_quoted($line); + if ($quoted === false) { + drupal_set_message(t("Translation file broken: Syntax error in line %lineno", array("%lineno" => $lineno)), 'error'); + return FALSE; + } + $current["msgstr"] = $quoted; + $context = "MSGSTR"; + } + elseif ($line != "") { + $quoted = _locale_import_parse_quoted($line); + if ($quoted === false) { + drupal_set_message(t("Translation file broken: Syntax error in line %lineno", array("%lineno" => $lineno)), 'error'); + return FALSE; + } + if (($context == "MSGID") || ($context == "MSGID_PLURAL")) { + $current["msgid"] .= $quoted; + } + elseif ($context == "MSGSTR") { + $current["msgstr"] .= $quoted; + } + elseif ($context == "MSGSTR_ARR") { + $current["msgstr"][$plural] .= $quoted; + } + else { + drupal_set_message(t("Translation file broken: Unexpected string in line %lineno", array("%lineno" => $lineno)), 'error'); + return FALSE; + } + } + } + + // End of PO file, flush last entry + if (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) { + $strings[$current["msgid"]] = $current; + } + elseif ($context != "COMMENT") { + drupal_set_message(t("Translation file broken: Unexpected end file at line %lineno", array("%lineno" => $lineno)), 'error'); + return FALSE; + } + + return $strings; +} + +/** + * Parses a Gettext Portable Object file header + * + * @param $header A string containing the complete header + * @return An associative array of key-value pairs + * @author Jacobo Tarrio + */ +function _locale_import_parse_header($header) { + $hdr = array(); + + $lines = explode("\n", $header); + foreach ($lines as $line) { + $line = trim($line); + if ($line) { + list($tag, $contents) = explode(":", $line, 2); + $hdr[trim($tag)] = trim($contents); + } + } + + return $hdr; +} + +/** + * Parses a Plural-Forms entry from a Gettext Portable Object file header + * + * @param $pluralforms A string containing the Plural-Forms entry + * @return An array containing the number of plurals and a + * formula in PHP for computing the plural form + * @author Jacobo Tarrio + */ +function _locale_import_parse_plural_forms($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, ";")) { + $nplurals = substr($nplurals, 9, strpos($nplurals, ";") - 9); + } + else { + return FALSE; + } + $plural = strstr($pluralforms, "plural="); + if (strpos($plural, ";")) { + $plural = substr($plural, 7, strpos($plural, ";") - 7); + } + else { + return FALSE; + } + + // Get PHP version of the plural formula + $plural = _locale_import_parse_arithmetic($plural); + + if ($plural) { + return array($nplurals, $plural); + } + else { + drupal_set_message(t("Translation file broken: Plural formula couldn't get parsed."), 'error'); + return FALSE; + } +} + +/** + * 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 A string containing the arithmetic formula + * @return The PHP version of the formula + * @author Jacobo Tarrio + */ +function _locale_import_parse_arithmetic($string) { + // Operator precedence table + $prec = array("(" => -1, ")" => -1, "?" => 1, ":" => 1, "||" => 3, "&&" => 4, "==" => 5, "!=" => 5, "<" => 6, ">" => 6, "<=" => 6, ">=" => 6, "+" => 7, "-" => 7, "*" => 8, "/" => 8, "%" => 8); + // Right associativity + $rasc = array("?" => 1, ":" => 1); + + $tokens = _locale_import_tokenize_formula($string); + + // Parse by converting into infix notation then back into postfix + $opstk = array(); + $elstk = array(); + + foreach ($tokens as $token) { + $ctok = $token; + + // Numbers and the $n variable are simply pushed into $elarr + if (is_numeric($token)) { + $elstk[] = $ctok; + } + elseif ($ctok == "n") { + $elstk[] = '$n'; + } + elseif ($ctok == "(") { + $opstk[] = $ctok; + } + elseif ($ctok == ")") { + $topop = array_pop($opstk); + while (($topop != NULL) && ($topop != "(")) { + $elstk[] = $topop; + $topop = array_pop($opstk); + } + } + elseif ($prec[$ctok]) { + // If it's an operator, then pop from $oparr into $elarr until the + // precedence in $oparr is less than current, then push into $oparr + $topop = array_pop($opstk); + while (($topop != NULL) && ($prec[$topop] >= $prec[$ctok]) && !(($prec[$topop] == $prec[$ctok]) && $rasc[$topop] && $rasc[$ctok])) { + $elstk[] = $topop; + $topop = array_pop($opstk); + } + if ($topop) { + $opstk[] = $topop; // Return element to top + } + $opstk[] = $ctok; // Parentheses are not needed + } + else { + return false; + } + } + + // Flush operator stack + $topop = array_pop($opstk); + while ($topop != NULL) { + $elstk[] = $topop; + $topop = array_pop($opstk); + } + + // Now extract formula from stack + $prevsize = count($elstk) + 1; + while (count($elstk) < $prevsize) { + $prevsize = count($elstk); + for ($i = 2; $i < count($elstk); $i++) { + $op = $elstk[$i]; + if ($prec[$op]) { + $f = ""; + if ($op == ":") { + $f = $elstk[$i - 2] ."):". $elstk[$i - 1] .")"; + } + elseif ($op == "?") { + $f = "(". $elstk[$i - 2] ."?(". $elstk[$i - 1]; + } + else { + $f = "(". $elstk[$i - 2] . $op . $elstk[$i - 1] .")"; + } + array_splice($elstk, $i - 2, 3, $f); + break; + } + } + } + + // If only one element is left, the number of operators is appropriate + if (count($elstk) == 1) { + return $elstk[0]; + } + else { + return FALSE; + } +} + +/** + * Backward compatible implementation of token_get_all() for formula parsing + * + * @param $string A string containing the arithmetic formula + * @return The PHP version of the formula + * @author Gerhard Killesreiter + */ +function _locale_import_tokenize_formula($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})) { // We won't have a space + $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; +} + +/** + * Modify a string to contain proper count indices + * + * This is a callback function used via array_map() + * + * @param $entry An array element + * @param $key Index of the array element + */ +function _locale_import_append_plural($entry, $key) { + // No modifications for 0, 1 + if ($key == 0 || $key == 1) { + return $entry; + } + + // First remove any possibly false indices, then add new ones + $entry = preg_replace('/(%count)\[[0-9]\]/', '\\1', $entry); + return preg_replace('/(%count)/', "\\1[$key]", $entry); +} + +/** + * Generate 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 + */ +function _locale_import_shorten_comments($comment) { + $comm = ''; + while(strlen($comm) < 128 && count($comment)) { + $comm .= substr(array_shift($comment), 1) .', '; + } + return substr($comm, 0, -2); +} + +/** + * Parses a string in quotes + * + * @param $string A string specified with enclosing quotes + * @return The string parsed from inside the quotes + */ +function _locale_import_parse_quoted($string) { + if (substr($string, 0, 1) != substr($string, -1, 1)) { + return FALSE; // Start and end quotes must be the same + } + $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 { + return FALSE; // Unrecognized quote + } +} + +/** + * User interface for the translation export screen + */ +function _locale_admin_export_screen() { + $languages = locale_supported_languages(FALSE, TRUE); + $languages = array_map("t", $languages['name']); + unset($languages['en']); + $output = ''; + + // Offer language specific export if any language is set up + if (count($languages)) { + $output .= '<h2>'. t('Export translation') .'</h2>'; + $form = form_select(t('Language name'), 'langcode', '', $languages, t('Select the language you would like to export in gettext Portable Object (.po) format.')); + $form .= form_submit(t('Export')); + $output .= form($form); + } + + // Complete template export of the strings + $output .= '<h2>'. t('Export template') .'</h2>'; + $form = t('<p>Generate a gettext Portable Object Template (.pot) file with all the interface strings from the Drupal locale database.</p>'); + $form .= form_submit(t('Export')); + $output .= form($form); + + return $output; +} + +/** + * Exports a Portable Object (Template) file for a language + * + * @param $language Selects a language to generate the output for + */ +function _locale_export_po($language) { + global $user; + + // Get language specific strings, or all strings + if ($language) { + $meta = db_fetch_object(db_query("SELECT * FROM {locales_meta} WHERE locale = '%s'", $language)); + $result = db_query("SELECT s.lid, s.source, s.location, t.translation, t.plid, t.plural FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid WHERE t.locale = '%s' ORDER BY t.plid, t.plural", $language); + } + else { + $result = db_query("SELECT s.lid, s.source, s.location, t.plid, t.plural FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid GROUP BY s.lid ORDER BY t.plid, t.plural"); + } + + // Build array out of the database results + $parent = array(); + while ($child = db_fetch_object($result)) { + $parent[$child->lid]['comment'] = $child->location; + $parent[$child->lid]['msgid'] = $child->source; + if ($child->plid) { + $parent[$child->lid][$child->plid]['plural'] = $child->lid; + $parent[$child->lid][$child->plid]['translation'] = $child->translation; + $parent[$child->lid][$child->plid]['msgid'] = $child->source; + } + else { + $parent[$child->lid]['translation'] = $child->translation; + } + } + + // Generating Portable Object file for a language + if ($language) { + $filename = $language .'.po'; + $header .= "# $meta->name translation of ". variable_get('site_name', 'Drupal') ."\n"; + $header .= '# Copyright (c) '. date('Y') .' '. $user->name .' <'. $user->mail .">\n"; + $header .= "#\n"; + $header .= "msgid \"\"\n"; + $header .= "msgstr \"\"\n"; + $header .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n"; + $header .= "\"POT-Creation-Date: ". date("Y-m-d H:iO") ."\\n\"\n"; + $header .= "\"PO-Revision-Date: ". date("Y-m-d H:iO") ."\\n\"\n"; + $header .= "\"Last-Translator: ". $user->name .' <'. $user->mail .">\\n\"\n"; + $header .= "\"Language-Team: ". $meta->name .' <'. $user->mail .">\\n\"\n"; + $header .= "\"MIME-Version: 1.0\\n\"\n"; + $header .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n"; + $header .= "\"Content-Transfer-Encoding: 8bit\\n\"\n"; + if ($meta->formula && $meta->plurals) { + $header .= "\"Plural-Forms: nplurals=". $meta->plurals ."; plural=". strtr($meta->formula, '$', '') .";\\n\"\n"; + } + $header .= "\n"; + watchdog('locale', strtr("PO file for locale '%loc' downloaded.", array('%loc' => $meta->name))); + } + + // Generating Portable Object Template + else { + $filename = variable_get('site_name', 'drupal') .'.pot'; + $header .= "# LANGUAGE translation of PROJECT\n"; + $header .= "# Copyright (c) YEAR NAME <EMAIL@ADDRESS>\n"; + $header .= "#\n"; + $header .= "msgid \"\"\n"; + $header .= "msgstr \"\"\n"; + $header .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n"; + $header .= "\"POT-Creation-Date: ". date("Y-m-d H:iO") ."\\n\"\n"; + $header .= "\"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\\n\"\n"; + $header .= "\"Last-Translator: NAME <EMAIL@ADDRESS>\\n\"\n"; + $header .= "\"Language-Team: LANGUAGE <EMAIL@ADDRESS>\\n\"\n"; + $header .= "\"MIME-Version: 1.0\\n\"\n"; + $header .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n"; + $header .= "\"Content-Transfer-Encoding: 8bit\\n\"\n"; + $header .= "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n"; + $header .= "\n"; + watchdog('locale', 'POT file downloaded.'); + } + + // Start download process + header("Content-Disposition: attachment; filename=$filename"); + header("Content-Type: text/plain; charset=utf-8"); + + print $header; + + foreach ($parent as $lid => $message) { + if (!isset($done[$lid])) { + if ($message['comment']) { + print '#: '. $message['comment'] ."\n"; + } + print 'msgid '. _locale_export_print($message['msgid']); + if (isset($message[1]['plural'])) { + print 'msgid_plural '. _locale_export_print($message[1]['msgid']); + if ($language) { + for ($i = 0; $i < $meta->plurals; $i++) { + print 'msgstr['. $i .'] '. _locale_export_print(_locale_export_remove_plural($message[${i}]['translation'])); + $done[$message[${i}]['plural']] = 1; + } + } + else { + print 'msgstr[0] ""'. "\n"; + print 'msgstr[1] ""'. "\n"; + $done[$message[0]['plural']] = 1; + $done[$message[1]['plural']] = 1; + } + } + else { + if ($language) { + print 'msgstr '. _locale_export_print($message['translation']); + } + else { + print 'msgstr ""'. "\n"; + } + } + print "\n"; + } + } + die(); +} + +/** + * Print out a string on multiple lines + */ +function _locale_export_print($str) { + $stri = addcslashes($str, "\0..\37\\\""); + $parts = array(); + + // Cut text into several lines + while ($stri != "") { + $i = strpos($stri, "\\n"); + if ($i === FALSE) { + $curstr = $stri; + $stri = ""; + } + else { + $curstr = substr($stri, 0, $i + 2); + $stri = substr($stri, $i + 2); + } + $curparts = explode("\n", _locale_export_wrap($curstr, 70)); + $parts = array_merge($parts, $curparts); + } + + if (count($parts) > 1) { + return "\"\"\n\"". implode("\"\n\"", $parts) ."\"\n"; + } + else { + return "\"$parts[0]\"\n"; + } +} + +/** + * Custom word wrapping for Portable Object (Template) files. + * + * @author Jacobo Tarrio + */ +function _locale_export_wrap($str, $len) { + $words = split(" ", $str); + $ret = array(); + + $cur = ""; + $nstr = 1; + while (count($words)) { + $word = array_shift($words); + if ($nstr) { + $cur = $word; + $nstr = 0; + } + elseif (strlen("$cur $word") > $len) { + $ret[] = $cur . " "; + $cur = $word; + } + else { + $cur = "$cur $word"; + } + } + $ret[] = $cur; + + return implode("\n", $ret); +} + +/** + * Removes plural index information from a string + */ +function _locale_export_remove_plural($entry) { + return preg_replace('/(%count)\[[0-9]\]/', '\\1', $entry); +} + +function _locale_string_delete($lid) { + db_query("DELETE FROM {locales_source} WHERE lid = %d", $lid); + db_query("DELETE FROM {locales_target} WHERE lid = %d", $lid); + locale_refresh_cache(); + drupal_set_message(t("deleted string")); +} + +/** + * Action handler for string editing + * + * Saves all translations of one string submitted from a form + */ +function _locale_string_save($lid) { + + $edit =& $_POST["edit"]; + foreach ($edit as $key => $value) { + $trans = db_fetch_object(db_query("SELECT translation FROM {locales_target} WHERE lid = %d AND locale = '%s'", $lid, $key)); + if ($trans->translation) { + db_query("UPDATE {locales_target} SET translation = '%s' WHERE lid = %d AND locale = '%s'", $value, $lid, $key); + } + else { + db_query("INSERT INTO {locales_target} (lid, translation, locale) VALUES (%d, '%s', '%s')", $lid, $value, $key); + } + } + locale_refresh_cache(); + // delete form data so it will remember where it came from + $edit = ''; + + drupal_set_message(t("saved string")); +} + +/** + * User interface for string editing + */ +function _locale_string_edit($lid) { + $languages = locale_supported_languages(FALSE, TRUE); + unset($languages['name']['en']); + + $result = db_query("SELECT DISTINCT s.source, t.translation, t.locale FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid WHERE s.lid = %d", $lid); + $form = ''; + while ($translation = db_fetch_object($result)) { + $orig = $translation->source; + $form .= (strlen($orig) > 40) ? form_textarea($languages['name'][$translation->locale], $translation->locale, $translation->translation, 70, 15) : form_textfield($languages['name'][$translation->locale], $translation->locale, $translation->translation, 50, 128); + unset($languages['name'][$translation->locale]); + } + foreach ($languages['name'] as $key => $lang) { + $form .= (strlen($orig) > 40) ? form_textarea($lang, $key, '', 70, 15) : form_textfield($lang, $key, '', 50, 128); + } + $form = form_item(t('Original text'), wordwrap(drupal_specialchars($orig, 0))) . $form; + + $form .= form_submit(t('Save translations')); + + return form($form); +} + +/** + * List languages in search result table + */ +function _locale_string_language_list($translation) { + $languages = locale_supported_languages(FALSE, TRUE); + unset($languages['name']['en']); + $output = ''; + foreach ($languages['name'] as $key => $value) { + if (isset($translation[$key])) { + $output .= ($translation[$key] != '') ? $key .' ' : "<strike>$key</strike> "; + } + } + + return $output; +} + +/** + * Build object out of search criteria specified in request variables + */ +function _locale_string_seek_query() { + static $query = NULL; + + if (is_null($query) && isset($_REQUEST['edit'])) { + $fields = array('string', 'language', 'searchin'); + $query = new StdClass; + if (is_array($_REQUEST['edit'])) { + foreach ($_REQUEST['edit'] as $key => $value) { + if (!empty($value) && in_array($key, $fields)) { + $query->$key = $value; + } + } + } + else { + foreach ($_REQUEST as $key => $value) { + if (!empty($value) && in_array($key, $fields)) { + $query->$key = strpos(',', $value) ? explode(',', $value) : $value; + } + } + } + } + return $query; +} + +/** + * Perform a string search and display results in a table + */ +function _locale_string_seek() { + // We have at least one criterium to match + if ($query = _locale_string_seek_query()) { + $join = "SELECT s.source, s.location, s.lid, t.translation, t.locale FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid "; + + // Compute LIKE section + switch ($query->searchin) { + case 'translated': + $where = "WHERE (t.translation LIKE '%". check_query($query->string) ."%' AND t.translation != '')"; + $orderby = "ORDER BY t.translation"; + break; + case 'untranslated': + $where = "WHERE (s.source LIKE '%". check_query($query->string) ."%' AND t.translation = '')"; + $orderby = "ORDER BY s.source"; + break; + case 'all' : + default: + $where = "WHERE (s.source LIKE '%". check_query($query->string) ."%' OR t.translation LIKE '%". check_query($query->string) ."%')"; + $orderby = ''; + break; + } + + switch ($query->language) { + // Force search in source strings + case "en": + $sql = $join ." WHERE s.source LIKE '%". check_query($query->string) ."%' ORDER BY s.source"; + break; + // Search in all languages + case "all": + $sql = "$join $where $orderby"; + break; + // Some different language + default: + $sql = "$join $where AND t.locale = '". check_query($query->language) ."' $orderby"; + } + + $result = pager_query($sql, 50); + + $header = array(t('string'), t('locales'), array('data' => t('operations'), 'colspan' => '2')); + $arr = array(); + while ($locale = db_fetch_object($result)) { + $arr[$locale->lid]['locales'][$locale->locale] = $locale->translation; + $arr[$locale->lid]['location'] = $locale->location; + $arr[$locale->lid]['source'] = $locale->source; + } + foreach ($arr as $lid => $value) { + $source = htmlspecialchars($value['source']); + $rows[] = array(array('data' => (strlen($source) > 150 ? substr($source, 0, 150) .'...' : $source) .'<br /><small>'. $value['location'] .'</small>'), array('data' => _locale_string_language_list($value['locales']), 'align' => 'center'), array('data' => l(t('edit'), "admin/locale/string/edit/$lid"), 'nowrap' => 'nowrap'), array('data' => l(t('delete'), "admin/locale/string/delete/$lid"), 'nowrap' => 'nowrap')); + } + + $request = array(); + if (count($query)) { + foreach ($query as $key => $value) { + $request[$key] = (is_array($value)) ? implode(',', $value) : $value; + } + } + + if ($pager = theme('pager', NULL, 50, 0, $request)) { + $rows[] = array(array('data' => "$pager", 'colspan' => '5')); + } + + $output .= theme('table', $header, $rows); + + } + + return $output; +} + +/** + * User interface for the string search screen + */ +function _locale_string_seek_form() { + + // Get *all* languages set up + $languages = locale_supported_languages(FALSE, TRUE); + asort($languages['name']); unset($languages['name']['en']); + + // Present edit form preserving previous user settings + $query = _locale_string_seek_query(); + $form .= form_textfield(t('Strings to search for'), 'string', $query->string, 30, 30, t('Leave blank to show all strings. The search is case sensitive.')); + $form .= form_radios(t('Language'), 'language', ($query->language ? $query->language : 'all'), array_merge(array('all' => t('All languages'), 'en' => t('English (provided by Drupal)')), $languages['name'])); + $form .= form_radios(t('Search in'), 'searchin', ($query->searchin ? $query->searchin : 'all'), array('all' => t('All strings in that language'), 'translated' => t('Only translated strings'), 'untranslated' => t('Only untranslated strings'))); + + $form .= form_submit(t('Search')); + $output = form(form_group(t('Search strings'), $form), 'POST', url('admin/locale/string/search')); + + return $output; +} + +// --------------------------------------------------------------------------------- +// List of some of the most common languages (administration only) + +/** + * Prepares the language code list for a select form item with only the unsupported ones + */ +function _locale_prepare_iso_list() { + $languages = locale_supported_languages(FALSE, TRUE); + $isocodes = _locale_get_iso639_list(); + foreach ($isocodes as $key => $value) { + if (isset($languages['name'][$key])) { + unset($isocodes[$key]); + continue; + } + if (count($value) == 2) { + $tname = t($value[0]); + $isocodes[$key] = ($tname == $value[1]) ? $tname : "$tname ($value[1])"; + } + else { + $isocodes[$key] = t($value[0]); + } + } + asort($isocodes); + return $isocodes; +} + +/** + * Some of the common languages with their English and native names + * + * Based on ISO 639 and http://people.w3.org/rishida/names/languages.html + */ +function _locale_get_iso639_list() { + return array( + "aa" => array("Afar"), + "ab" => array("Abkhazian", "аҧсуа бызшәа"), + "ae" => array("Avestan"), + "af" => array("Afrikaans"), + "ak" => array("Akan"), + "am" => array("Amharic", "አማርኛ"), + "ar" => array("Arabic", "العربية"), + "as" => array("Assamese"), + "av" => array("Avar"), + "ay" => array("Aymara"), + "az" => array("Azerbaijani", "azərbaycan"), + "ba" => array("Bashkir"), + "be" => array("Belarusian", "Беларуская"), + "bg" => array("Bulgarian", "Български"), + "bh" => array("Bihari"), + "bi" => array("Bislama"), + "bm" => array("Bambara", "Bamanankan"), + "bn" => array("Bengali"), + "bo" => array("Tibetan"), + "br" => array("Breton"), + "bs" => array("Bosnian", "Bosanski"), + "ca" => array("Catalan", "Català"), + "ce" => array("Chechen"), + "ch" => array("Chamorro"), + "co" => array("Corsican"), + "cr" => array("Cree"), + "cs" => array("Czech", "Čeština"), + "cu" => array("Old Slavonic"), + "cv" => array("Welsh", "Cymraeg"), + "cy" => array("Welch"), + "da" => array("Danish"), + "de" => array("German", "Deutsch"), + "dv" => array("Maldivian"), + "dz" => array("Bhutani"), + "ee" => array("Ewe", "Ɛʋɛ"), + "el" => array("Greek", "Ελληνικά"), + "en" => array("English"), + "eo" => array("Esperanto"), + "es" => array("Spanish", "Español"), + "et" => array("Estonian", "Eesti"), + "eu" => array("Basque", "Euskera"), + "fa" => array("Persian", "فارسی"), + "ff" => array("Fulah", "Fulfulde"), + "fi" => array("Finnish", "Suomi"), + "fj" => array("Fiji"), + "fo" => array("Faeroese"), + "fr" => array("French", "Français"), + "fy" => array("Frisian", "Frysk"), + "ga" => array("Irish", "Gaeilge"), + "gd" => array("Scots Gaelic"), + "gl" => array("Galician", "Galego"), + "gn" => array("Guarani"), + "gu" => array("Gujarati"), + "gv" => array("Manx"), + "ha" => array("Hausa"), + "he" => array("Hebrew", "עברית"), + "hi" => array("Hindi", "हिन्दी"), + "ho" => array("Hiri Motu"), + "hr" => array("Croatian", "Hrvatski"), + "hu" => array("Hungarian", "Magyar"), + "hy" => array("Armenian", "Հայերեն"), + "hz" => array("Herero"), + "ia" => array("Interlingua"), + "id" => array("Indonesian", "Bahasa Indonesia"), + "ie" => array("Interlingue"), + "ig" => array("Igbo"), + "ik" => array("Inupiak"), + "is" => array("Icelandic", "Íslenska"), + "it" => array("Italian", "Italiano"), + "iu" => array("Inuktitut"), + "ja" => array("Japanese", "日本語"), + "jv" => array("Javanese"), + "ka" => array("Georgian"), + "kg" => array("Kongo"), + "ki" => array("Kikuyu"), + "kj" => array("Kwanyama"), + "kk" => array("Kazakh", "Қазақ"), + "kl" => array("Greenlandic"), + "km" => array("Cambodian"), + "kn" => array("Kannada", "ಕನ್ನಡ"), + "ko" => array("Korean", "한국어"), + "kr" => array("Kanuri"), + "ks" => array("Kashmiri"), + "ku" => array("Kurdish", "Kurdî"), + "kv" => array("Komi"), + "kw" => array("Cornish"), + "ky" => array("Kirghiz", "Кыргыз"), + "la" => array("Latin", "Latina"), + "lb" => array("Luxembourgish"), + "lg" => array("Luganda"), + "ln" => array("Lingala"), + "lo" => array("Laothian"), + "lt" => array("Lithuanian", "Lietuviškai"), + "lv" => array("Latvian", "Latviešu"), + "mg" => array("Malagasy"), + "mh" => array("Marshallese"), + "mi" => array("Maori"), + "mk" => array("Macedonian", "Македонски"), + "ml" => array("Malayalam", "മലയാളം"), + "mn" => array("Mongolian"), + "mo" => array("Moldavian"), + "mr" => array("Marathi"), + "ms" => array("Malay", "Bahasa Melayu"), + "mt" => array("Maltese", "Malti"), + "my" => array("Burmese"), + "na" => array("Nauru"), + "nd" => array("North Ndebele"), + "ne" => array("Nepali"), + "ng" => array("Ndonga"), + "nl" => array("Dutch", "Nederlands"), + "no" => array("Norwegian", "Norsk"), + "nr" => array("South Ndebele"), + "nv" => array("Navajo"), + "ny" => array("Chichewa"), + "oc" => array("Occitan"), + "om" => array("Oromo"), + "or" => array("Oriya"), + "os" => array("Ossetian"), + "pa" => array("Punjabi"), + "pi" => array("Pali"), + "pl" => array("Polish", "Polski"), + "ps" => array("Pashto", "پښتو"), + "pt" => array("Portuguese", "Português"), + "qu" => array("Quechua"), + "rm" => array("Rhaeto-Romance"), + "rn" => array("Kirundi"), + "ro" => array("Romanian", "Română"), + "ru" => array("Russian", "Русский"), + "rw" => array("Kinyarwanda"), + "sa" => array("Sanskrit"), + "sc" => array("Sardinian"), + "sd" => array("Sindhi"), + "se" => array("Northern Sami"), + "sg" => array("Sango"), + "sh" => array("Serbo-Croatian"), + "si" => array("Singhalese"), + "sk" => array("Slovak", "Slovenčina"), + "sl" => array("Slovenian", "Slovenščina"), + "sm" => array("Samoan"), + "sn" => array("Shona"), + "so" => array("Somali"), + "sq" => array("Albanian", "Shqip"), + "sr" => array("Serbian", "Српски"), + "ss" => array("Siswati"), + "st" => array("Sesotho"), + "su" => array("Sudanese"), + "sv" => array("Swedish", "Svenska"), + "sw" => array("Swahili", "Kiswahili"), + "ta" => array("Tamil", "தமிழ்"), + "te" => array("Telugu", "తెలుగు"), + "tg" => array("Tajik"), + "th" => array("Thai", "ภาษาไทย"), + "ti" => array("Tigrinya"), + "tk" => array("Turkmen"), + "tl" => array("Tagalog"), + "tn" => array("Setswana"), + "to" => array("Tonga"), + "tr" => array("Turkish", "Türkçe"), + "ts" => array("Tsonga"), + "tt" => array("Tatar", "Tatarça"), + "tw" => array("Twi"), + "ty" => array("Tahitian"), + "ug" => array("Uighur"), + "uk" => array("Ukrainian", "Українська"), + "ur" => array("Urdu", "اردو"), + "uz" => array("Uzbek", "o'zbek"), + "ve" => array("Venda"), + "vi" => array("Vietnamese", "Tiếng Việt"), + "vo" => array("Volapük"), + "wo" => array("Wolof"), + "xh" => array("Xhosa", "isiXhosa"), + "yi" => array("Yiddish"), + "yo" => array("Yoruba", "Yorùbá"), + "za" => array("Zhuang"), + "zh_hans" => array("Chinese, Simplified", "简体中文"), + "zh_hant" => array("Chinese, Traditional", "繁體中文"), + "zu" => array("Zulu", "isiZulu"), + ); +} + +?> |