' . t('This module adds support for other modules to translate user defined strings. Depending on which modules you have enabled that use this feature you may see different text groups to translate.') .'

'; $output .= '

' . t('This works differently to Drupal standard localization system: The strings will be translated from the default language (which may not be English), so changing the default language may cause all these translations to be broken.') . '

'; $output .= ''; $output .= '

'. t('Read more on the Internationalization handbook: Translating user defined strings.') .'

'; return $output; case 'admin/build/translate/refresh': $output = '

'. t('On this page you can refresh and update values for user defined strings.') .'

'; $output .= ''; $output .= '

'. t('To search and translate strings, use the translation interface pages.', array('@translate-interface' => url('admin/build/translate'))) .'

'; return $output; case 'admin/settings/language': $output = '

'. t('Warning: Changing the default language may have unwanted effects on string translations. Read more about String translation', array('@i18nstrings-help' => url('admin/help/i18nstrings'))) .'

'; return $output; } } /** * Implementation of hook_menu(). */ function i18nstrings_menu() { $items['admin/build/translate/refresh'] = array( 'title' => 'Refresh', 'weight' => 20, 'type' => MENU_LOCAL_TASK, 'page callback' => 'i18nstrings_admin_refresh_page', 'file' => 'i18nstrings.admin.inc', 'access arguments' => array('translate interface'), ); return $items; } /** * Implementation of hook_form_alter(); * * Add English language in some string forms when it is not the default. */ function i18nstrings_form_alter(&$form, $form_state, $form_id) { switch ($form_id) { case 'locale_translate_export_po_form': case 'locale_translate_import_form': $names = locale_language_list('name', TRUE); if (language_default('language') != 'en' && array_key_exists('en', $names)) { if (isset($form['export'])) { $form['export']['langcode']['#options']['en'] = $names['en']; } else { $form['import']['langcode']['#options'][t('Already added languages')]['en'] = $names['en']; } } break; } } /** * Translate configurable string. * * @param $name * Textgroup and location glued with ':'. * @param $string * String in default language. Default language may or may not be English. * @param $langcode * Optional language code if different from current request language. * @param $update * Whether to force update/create for the string. */ function tt($name, $string, $langcode = NULL, $update = FALSE) { global $language; $langcode = $langcode ? $langcode : $language->language; if ($update) { i18nstrings_update_string($name, $string); } // If language is default, just return if (language_default('language') == $langcode) { return $string; } else { return i18nstrings_tt($name, $string, $langcode, FALSE); } } /** * 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) { $context = i18nstrings_context($name, $string); $context = implode('/', (array)$context); return tt($name, $string, $langcode, $update) .'[T:'. $string .'('. $context .')]'; } /** * Translate configurable string. * * Support for l10n client (patch pending for l10n client). * * @param $name * Textgroup and location glued with ':'. */ function i18nstrings_tt($name, $string, $langcode, $update = FALSE) { global $language, $l10n_client_strings; $context = i18nstrings_context($name, $string); if ($update) { i18nstrings_update_string($context, $string); } $translation = i18nstrings_get_string($context, $langcode, $string, !$update); if ($translation === FALSE) { // If the string is missing, create it. if (!$update) { i18nstrings_add_string($name, $string); i18nstrings_cache($context, $langcode, $string, TRUE); } $translation = TRUE; } // If current language add to l10n list for later on page translation. // If language were the default one we are not supossed to reach here. if ($language->language == $langcode) { $l10n_client_strings[$string] = $translation; } return ($translation === TRUE) ? $string : $translation; } /** * 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; $context->location = i18nstrings_location($context); 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, $string); 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, $string); $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); // Mysql just gets last id for latest query ? $source->lid = db_last_insert_id('locales_source', 'lid'); // Clear locale cache so this string can be added in a later request. cache_clear_all('locale:'. $context->textgroup .':', 'cache', TRUE); // Create string. $status = SAVED_NEW; } // Update metadata. db_query("DELETE FROM {i18n_strings} WHERE lid = %d", $source->lid); db_query("INSERT INTO {i18n_strings} (lid, type, objectid, property) VALUES(%d, '%s', %d, '%s')", $source->lid, $context->type, $context->objectid, $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, $string); // 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.objectid, 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.objectid, 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', 'objectid', '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.objectid, 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', objectid = '%s', property = '%s' WHERE lid = %d", $context->type, $context->objectid, $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 variable. 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_range("UPDATE {locales_source} SET version = '%s' WHERE lid = %d", VERSION, $translation->lid, 0, 1); 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->objectid = 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. * @param $string * For some textgroups and objects that don't have ids we use the string itself as index. * @return * Context object with textgroup, type, objectid, 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, $objectid, $property) = split(':', $context .':::'); $context = (object)array( 'textgroup' => $textgroup, 'type' => $type, 'objectid' => $objectid ? $objectid : 0, 'property' => $property ? $property : 0, ); $context->location = i18nstrings_location($context); if (!$context->objectid && !$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->objectid) { $location[] = $context->objectid; 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.objectid, 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, $string); if (!$context->objectid && $context->source) { // This is a type indexed by string. $context->objectid = $context->source; } // At this point context must have at least textgroup and type. if ($translation) { if ($context->property) { $strings[$langcode][$context->textgroup][$context->type][$context->objectid][$context->property] = $translation; } elseif ($context->objectid) { $strings[$langcode][$context->textgroup][$context->type][$context->objectid] = $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', 'objectid', '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; }