diff options
author | Dries Buytaert <dries@buytaert.net> | 2004-08-11 11:26:20 +0000 |
---|---|---|
committer | Dries Buytaert <dries@buytaert.net> | 2004-08-11 11:26:20 +0000 |
commit | 1831e1b690f02d7f551d38ef88a0ba200f786497 (patch) | |
tree | cc3805acedb24888afbd7cd76129e690c0c81d0e /includes | |
parent | 8517e17e70db3d80410a9020d378587f93e74d14 (diff) | |
download | brdo-1831e1b690f02d7f551d38ef88a0ba200f786497.tar.gz brdo-1831e1b690f02d7f551d38ef88a0ba200f786497.tar.bz2 |
- New locale module thanks to Gerhard, Goba, Marco, Kristjan and others.
The new locale module provides every functionality on the web interface, so you don't need to edit the configuration files or add columns, when you add a new language. This module is an integration of the old locale and localegettext modules, plus a bunch of logic to parse Gettext Portable Object files (opposed to Machine Object files, as supported by localegettext).
Note: I made some minor changes to the context-sensitive help texts and to some of the status messages.
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"), + ); +} + +?> |