language; // If language is default, just return if (language_default('language') == $langcode) { if ($update) { i18nstrings_update_string($name, $string); } return $string; } else { return i18nstrings_tt($name, $string, $langcode, $update); } } /** * Get configurable string, * * The difference with tt() is that it doesn't use a default string, it will be retrieved too. * This is used for source texts that we don't have stored anywhere else. * * As the original language string will be stored in locales too so it should be only used when updating */ function ts($name, $string = '', $langcode = NULL, $update = FALSE) { global $language; $langcode = $langcode ? $langcode : $language->language; $translation = NULL; if ($update) { i18nstrings_update_string($name, $string); } // if language is default look in sources table if (language_default('language') != $langcode) { $translation = i18nstrings_get_string($name, $langcode); } if (!$translation) { if ($source = i18nstrings_get_source($name)) { $translation = $source->source; } else { $translation = ''; } } return $translation; } /** * Debug util. Marks the translated strings */ function ttd($name, $string, $langcode = NULL, $update = FALSE) { return tt($name, $string, $langcode, $update).'[T:'.$string.']'; } /** * Translate configurable string * @param $name * Textgroup and location glued with ':' */ function i18nstrings_tt($name, $string, $langcode, $update = FALSE, $create = TRUE) { $context = i18nstrings_context($name, $string); if ($update) { i18nstrings_update_string($context, $string); } $translation = i18nstrings_get_string($context, $langcode); if ($translation) { return ($translation === TRUE) ? $string : $translation; } elseif ($translation === FALSE && $create) { i18nstrings_update_string($context, $string); i18nstrings_cache($context, $string, $langcode, TRUE); } // Default case, just return untranslated string return $string; } /** * Translate object */ function to($context, &$object, $properties = array(), $langcode = NULL, $update = FALSE) { global $language; $langcode = $langcode ? $langcode : $language->language; // If language is default, just return if (language_default('language') == $langcode && !$update) { return $object; } else { i18nstrings_to($context, $object, $properties, $langcode, $update); } } /** * Translate object properties */ function i18nstrings_to($context, &$object, $properties = array(), $langcode = NULL, $update = FALSE, $create = TRUE) { $context = i18nstrings_context($context); // @ TODO Object prefetch foreach ($properties as $property) { $context->property = $property; if (!empty($object->$property)) { $object->property = i18nstrings_tt($context, $object->$property, $langcode, $update, $create); } } } /** * Update / create / remove string * * @param $context * String context * @pram $string * New value of string for update/create. May be empty for removing. */ function i18nstrings_update_string($context, $string) { $context = i18nstrings_context($context); if ($string) { $status = i18nstrings_add_string($context, $string); } else { $status = i18nstrings_remove_string($context); } $params = array( '%location' => i18nstrings_location($context), '%textgroup' => $context->textgroup, '%string' => $string, ); switch ($status) { case SAVED_UPDATED: drupal_set_message(t('Updated string %location for textgroup %textgroup: %string', $params)); break; case SAVED_NEW: drupal_set_message(t('Created string %location for text group %textgroup: %string', $params)); break; } return $status; } /** * Update string translation */ function i18nstrings_update_translation($context, $langcode, $translation) { if ($source = i18nstrings_get_source($context, $translation)) { db_query("INSERT INTO {locales_target}(lid, language, translation) VALUES(%d, '%s', '%s')", $source->lid, $langcode, $translation); } } /** * Add string * * This function checks for already existing string without context for this textgroup * * @return * Update status */ function i18nstrings_add_string($name, $string) { $context = i18nstrings_context($name); $location = i18nstrings_location($context); // Check if we have a source string $source = i18nstrings_get_source($context, $string); $status = -1; if ($source) { if ($source->source != $string || $source->location != $location) { // String has changed or didnt have location db_query("UPDATE {locales_source} SET source = '%s' WHERE lid = %d", $string, $source->lid); $status = SAVED_UPDATED; } } else { db_query("INSERT INTO {locales_source} (location, source, textgroup, version) VALUES ('%s', '%s', '%s', '%s')", $location, $string, $context->textgroup, 0); // Clear locale cache so this string can be added in a later request. cache_clear_all('locale:'.$context->textgroup.':', 'cache', TRUE); // Create string $source->lid = db_last_insert_id('locales_source', 'lid'); $status = SAVED_NEW; } // Update metadata db_query("DELETE FROM {i18n_strings} WHERE lid = %d", $source->lid); db_query("INSERT INTO {i18n_strings} (lid, type, oid, property) VALUES(%d, '%s', %d, '%s')", $source->lid, $context->type, $context->oid, $context->property); return $status; } /** * Get source string provided a string context * * This will search first with the full context parameters and, if not found, * it will search again only with textgroup and source string * * @param $context * Context string or object * @return * Context object if it exists */ function i18nstrings_get_source($context, $string = NULL) { $context = i18nstrings_context($context); // Check if we have the string for this location list($where, $args) = i18nstrings_context_query($context); if ($source = db_fetch_object(db_query("SELECT s.*, i.type, i.oid, i.property FROM {locales_source} s LEFT JOIN {i18n_strings} i ON s.lid = i.lid WHERE ". implode(' AND ', $where), $args))) { $source->context = $context; return $source; } // Search for the same string for this textgroup without object data if ($string && $source = db_fetch_object(db_query("SELECT s.*, i.type, i.oid, i.property FROM {locales_source} s LEFT JOIN {i18n_strings} i ON s.lid = i.lid WHERE s.textgroup = '%s' AND s.source = '%s' AND i.lid IS NULL", $context->textgroup, $string))) { $source->context = NULL; return $source; } } /** * Get string for a language * * @param $context * Context string or object * @param $langcode * Language code to retrieve string for * * @return * - translation if found * - TRUE if not found and cached * - FALSE if not even cached * */ function i18nstrings_get_string($context, $langcode) { $context = i18nstrings_context($context); if ($translation = i18nstrings_cache($context, $langcode)) { return $translation; } else { // Search translation and add it to the cache list($where, $args) = i18nstrings_context_query($context); $where[] = "t.language = '%s'"; $args[] = $langcode; $text = db_fetch_object(db_query("SELECT s.*, t.translation FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid WHERE ". implode(' AND ', $where), $args)); if ($text && $text->translation) { i18nstrings_cache($context, $langcode, NULL, $text->translation); return $text->translation; } else { i18nstrings_cache($context, $langcode, NULL, TRUE); return $text ? NULL : FALSE ; } } } /** * Remove string for a given context */ function i18nstrings_remove_string($context, $string = NULL) { if ($source = i18nstrings_get_source($context, $string)) { db_query("DELETE FROM {locales_target} WHERE lid = %d", $source->lid); db_query("DELETE FROM {i18n_strings} WHERE lid = %d", $source->lid); db_query("DELETE FROM {locales_source} WHERE lid = %d", $source->lid); cache_clear_all('locale:'.$context->textgroup.':', 'cache', TRUE); return SAVED_DELETED; } } /** * Update context for strings. * * As some string locations depend on configurable values, the field needs sometimes to be updated * without losing existing translations. I.e: * - profile fields indexed by field name * - content types indexted by low level content type name * * Example * 'profile:field:oldfield:*' -> 'profile:field:newfield:*' */ function i18nstrings_update_context($oldname, $newname) { // Get context replacing '*' with empty string $oldcontext = i18nstrings_context(str_replace('*', '', $oldname)); $newcontext = i18nstrings_context(str_replace('*', '', $newname)); // Get location with placeholders $location = i18nstrings_location(str_replace('*', '%', $oldname)); foreach (array('textgroup', 'type', 'oid', 'property') as $field) { if ((!empty($oldcontext->$field) || !empty($newcontext->$field)) && $oldcontext->$field != $newcontext->$field) { $replace[$field] = $newcontext->$field; } } // Query and replace $result = db_query("SELECT s.*, i.type, i.oid, i.property FROM {locales_source} s LEFT JOIN {i18n_strings} i ON s.lid = i.lid WHERE s.textgroup = '%s' AND s.location LIKE '%s'", $oldcontext->textgroup, $location); while ($source = db_fetch_object($result)) { // Make sure we have string and context $context = i18nstrings_context($oldcontext->textgroup.':'.$source->location); foreach ($replace as $field => $value) { $context->$field = $value; } // Update source string db_query("UPDATE {locales_source} SET textgroup = '%s', location = '%s' WHERE lid = %d", $context->textgroup, i18nstrings_location($context), $source->lid); // Update object data db_query("UPDATE {i18n_strings} SET type = '%s', oid = '%s', property = '%s' WHERE lid = %d", $context->type, $context->oid, $context->property, $source->lid); } drupal_set_message(t('Updating string names from %oldname to %newname', array('%oldname' => $oldname, '%newname' => $newname))); } /** * Provides interface translation services. * * This function is called from tt() to translate a string if needed. * * @param $textgroup * * @param $string * A string to look up translation for. If omitted, all the * cached strings will be returned in all languages already * used on the page. * @param $langcode * Language code to use for the lookup. */ function i18nstrings_textgroup($textgroup, $string = NULL, $langcode = NULL) { global $language; static $locale_t; // Return all cached strings if no string was specified if (!isset($string)) { return isset($locale_t[$textgroup]) ? $locale_t[$textgroup] : array(); } $langcode = isset($langcode) ? $langcode : $language->language; // Store database cached translations in a static var. if (!isset($locale_t[$langcode])) { $locale_t[$langcode] = array(); // Disabling the usage of string caching allows a module to watch for // the exact list of strings used on a page. From a performance // perspective that is a really bad idea, so we have no user // interface for this. Be careful when turning this option off! if (variable_get('locale_cache_strings', 1) == 1) { if ($cache = cache_get('locale:'.$textgroup.':'.$langcode, 'cache')) { $locale_t[$textgroup][$langcode] = $cache->data; } else { // Refresh database stored cache of translations for given language. // We only store short strings used in current version, to improve // performance and consume less memory. $result = db_query("SELECT s.source, t.translation, t.language FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = '%s' WHERE s.textgroup = '%s' AND s.version = '%s' AND LENGTH(s.source) < 75", $langcode, $textgroup, VERSION); while ($data = db_fetch_object($result)) { $locale_t[$textgroup][$langcode][$data->source] = (empty($data->translation) ? TRUE : $data->translation); } cache_set('locale:'.$textgroup.':'. $langcode, $locale_t[$textgroup][$langcode]); } } } // If we have the translation cached, skip checking the database if (!isset($locale_t[$textgroup][$langcode][$string])) { // We do not have this translation cached, so get it from the DB. $translation = db_fetch_object(db_query("SELECT s.lid, t.translation, s.version FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = '%s' WHERE s.source = '%s' AND s.textgroup = '%s'", $langcode, $string, $textgroup)); if ($translation) { // We have the source string at least. // Cache translation string or TRUE if no translation exists. $locale_t[$textgroup][$langcode][$string] = (empty($translation->translation) ? TRUE : $translation->translation); if ($translation->version != VERSION) { // This is the first use of this string under current Drupal version. Save version // and clear cache, to include the string into caching next time. Saved version is // also a string-history information for later pruning of the tables. db_query("UPDATE {locales_source} SET version = '%s' WHERE lid = %d LIMIT 1", VERSION, $translation->lid); cache_clear_all('locale:'.$textgroup.':', 'cache', TRUE); } } else { // We don't have the source string, cache this as untranslated. db_query("INSERT INTO {locales_source} (location, source, textgroup, version) VALUES ('%s', '%s', 'default', '%s')", request_uri(), $string, VERSION); $locale_t[$langcode][$string] = TRUE; // Clear locale cache so this string can be added in a later request. cache_clear_all('locale:'.$textgroup.':', 'cache', TRUE); } } return ($locale_t[$textgroup][$langcode][$string] === TRUE ? $string : $locale_t[$textgroup][$langcode][$string]); } /** * Convert context string in a context object * * I.e. * 'taxonomy:term:1:name' * will become a $context object where * $context->textgroup = 'taxonomy'; * $context->type = 'term'; * $context->oid = 1; * $context->property = 'name'; * Examples: * 'taxonomy:title' -> (taxonomy, title, 0, 0) * 'contenttypes:type:[type]:name' * 'contenttypes:type:[type]:description' * 'profile:category' * 'profile:field:[fid]:title' * * @param $context * Context string or object * @return * Context object with textgroup, type, oid, property and location names */ function i18nstrings_context($context, $string = NULL) { // Context may be already an object if (is_object($context)) { return $context; } else { // We add empty fields at the end before splitting list($textgroup, $type, $oid, $property) = split(':', $context.':::'); $context = (object)array( 'textgroup' => $textgroup, 'type' => $type, 'oid' => $oid ? $oid : 0, 'property' => $property ? $property : 0, ); $context->location = i18nstrings_location($context); if (!$context->oid && !$context->property && $string) { $context->source = $string; } return $context; } } /** * Get query conditions for this context */ function i18nstrings_context_query($context, $alias = 's') { $where = array("$alias.textgroup = '%s'", "$alias.location = '%s'"); $args = array($context->textgroup, $context->location); if (!empty($context->source)) { $where[] = "s.source = '%s'"; $args[] = $context->source; } return array($where, $args); } /** * Get location string from context * * Returns the location for the locale table for a string context */ function i18nstrings_location($context) { if (is_string($context)){ $context = i18nstrings_context($context); } $location[] = $context->type; if ($context->oid) { $location[] = $context->oid; if ($context->property) { $location[] = $context->property; } } return implode(':', $location); } /** * Prefetch a number of object strings */ function i18nstrings_prefetch($context, $langcode = NULL, $join = array(), $conditions = array()) { global $language; $langcode = $langcode ? $langcode : $language->language; // Add language condition $conditions['t.language'] = $langcode; // Get context conditions $context = (array)i18nstrings_context($context); foreach ($context as $key => $value) { if ($value) { if ($key == 'textgroup') { $conditions['s.textgroup'] = $value; } else { $conditions['i.'. $key] = $value; } } } // Prepare where clause $where = $params = array(); foreach ($conditions as $key => $value) { if (is_array($value)) { $where[] = $key . ' IN ('.db_placeholders($value, is_int($value[0]) ? 'int' : 'string').')'; $params = array_merge($params, $value); } else { $where[] = $key . ' = ' . is_int($value) ? '%d' : "'%s'"; $params[] = $value; } } $sql = "SELECT s.textgroup, s.source, i.type, i.oid, i.property, t.translation FROM {locales_source} s"; $sql .=" INNER JOIN {i18n_strings} i ON s.lid = i.lid INNER JOIN {locales_target} t ON s.lid = t.lid "; $sql .= implode(' ', $join) . ' ' . implode (' AND ', $where); $result = db_query($sql, $params); // Fetch all rows and store in cache while ($t = db_fetch_object($result)) { i18nstrings_cache($t, $langcode, $t->source, $t->translation); } } /** * Retrieves and stores translations in page (static variable) cache. */ function i18nstrings_cache($context, $langcode, $string = NULL, $translation = NULL) { static $strings; $context = i18nstrings_context($context); if (!$context->oid && $string) { // This is a type indexed by string $context->oid = $string; } // At this point context must have at least textgroup and type if ($translation) { if ($context->property) { $strings[$langcode][$context->textgroup][$context->type][$context->oid][$context->property] = $translation; } elseif ($context->oid) { $strings[$langcode][$context->textgroup][$context->type][$context->oid] = $translation; } else { $strings[$langcode][$context->textgroup][$context->type] = $translation; } } else { // Search up the tree for the object or a default $search = &$strings[$langcode]; $default = NULL; $list = array('textgroup', 'type', 'oid', 'property'); while (($field = array_shift($list)) && !empty($context->$field)) { if (isset($search[$context->$field])) { $search = &$search[$context->$field]; if (isset($search['#default'])) { $default = $search['#default']; } } else { // We dont have cached this tree so we return the default return $default; } } // Returns the part of the array we got to return $search; } } /** * Callback for menu title translation */ function i18nstrings_title_callback($name, $string, $callback = NULL) { $string = tt($name, $string); if ($callback) { $string = $callback($string); } return $string; } /** * Implementation of hook_menu(). * function i18nstrings_menu($may_cache) { $items = array(); if ($may_cache) { $items[] = array( 'path' => 'admin/settings/i18n/strings', 'type' => MENU_LOCAL_TASK, 'title' => t('Strings'), 'description' => t('Translatable strings.'), 'callback' => 'i18nstrings_admin', 'access' => user_access('administer site configuration'), ); } else { } return $items; } /** * Menu callback. Administration page * function i18nstrings_admin($op = NULL, $strid = NULL) { switch($op) { case 'edit': return drupal_get_form('i18nstrings_admin_form', $strid); default: return i18nstrings_admin_overview(); } } /** * List of strings * function i18nstrings_admin_overview() { $output = ''; $header = array(t('String Id'), t('Default'), ''); $result = db_query("SELECT DISTINCT(strid) FROM {i18n_strings} ORDER BY strid", i18n_default_language()); $rows = array(); while($str = db_fetch_object($result)) { $rows[] = array( $str->strid, tt($str->strid), l(t('edit'), 'admin/settings/i18n/strings/edit/'.$str->strid) ); } $output .= theme('table', $header, $rows); return $output; } /** * Form callback: i18nstrings_admin_form * function i18nstrings_admin_form($strid) { $strings = i18nstrings_load($strid); $form['strid'] = array('#type' => 'value', '#value' => $strid); $form['languages'] = array('#type' => 'fieldset', '#tree' => TRUE, '#title' => t('Translations')); // Approximate the number of rows in a textfield with a maximum of 10. $default = i18nstrings_get_string($strid, i18n_default_language()); $rows = min(ceil(str_word_count($default) / 12), 10); foreach (i18n_supported_languages() as $language => $name) { $form['languages'][$language] = array( '#type' => 'textarea', '#rows' => $rows, '#title' => $name, '#default_value' => i18nstrings_get_string($strid, $language) ); } $form['submit'] = array('#type' => 'submit', '#value' => t('Save')); return $form; } /** * Form submit callback * function i18nstrings_admin_form_submit($form_id, $form_values) { $strid = $form_values['strid']; foreach (i18n_supported_languages() as $language => $name) { i18nstrings_save_string($strid, $language, $form_values['languages'][$language]); } drupal_set_message(t('The strings have been updated')); return 'admin/settings/i18n/strings'; } /** * Load string translations * function i18nstrings_load($strid) { $strings = array(); $result = db_query("SELECT * FROM {i18n_strings} WHERE strid = '%s'", $strid); while ($str = db_fetch_object($result)) { $strings[$str->locale] = $str->text; } } /** * Save string for a language * function i18nstrings_save_string($strid, $language, $value) { drupal_set_message("DEBUG: i18n_strings_save: $strid($language)= $value"); db_query("DELETE FROM {i18n_strings} WHERE strid = '%s' AND locale = '%s'", $strid, $language); db_query("INSERT INTO {i18n_strings}(strid, locale, text) VALUES('%s', '%s', '%s')", $strid, $language, $value); } /**/