' . 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'))) .'

'; $output .= '

' . t('Important: To configure which Input formats are safe for translation, visit the configure strings page before refreshing your strings.', array('@configure-strings' => url('admin/settings/language/configure/strings'))) . '

'; 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; case 'admin/settings/language/configure/strings': $output = '

' . t('When translating user defined strings that have an Input format associated, translators will be able to edit the text before it is filtered which may be a security risk for some filters. An obvious example is when using the PHP filter but other filters may also be dangerous.') . '

'; $output .= '

' . t('As a general rule do not allow any filtered text to be translated unless the translators already have access to that Input format. However if you are doing all your translations through this site\'s translation UI or the Localization client, and never importing translations for other textgroups than default, filter access will be checked for translators on every translation page.') . '

'; $output .= '

' . t('Important: After disallowing some Input format, use the refresh strings page so forbidden strings are deleted and not allowed anymore for translators.', array('@refresh-strings' => url('admin/build/translate/refresh'))) . '

'; return $output; case 'admin/settings/filters': return '

' . t('After updating your Input formats do not forget to review the list of formats allowed for string translations on the configure translatable strings page.', array('@configure-strings' => url('admin/settings/language/configure/strings'))) . '

'; } } /** * 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'), ); $items['admin/settings/language/configure/strings'] = array( 'title' => 'String translation', 'weight' => 20, 'type' => MENU_LOCAL_TASK, 'page callback' => 'drupal_get_form', 'page arguments' => array('i18nstrings_admin_settings'), 'file' => 'i18nstrings.admin.inc', 'access arguments' => array('administer filters'), ); // AJAX callback path for strings. $items['i18nstrings/save'] = array( 'title' => 'Save string', 'page callback' => 'i18nstrings_save_string', 'access arguments' => array('use on-page translation'), 'type' => MENU_CALLBACK, ); 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; case 'locale_translate_edit_form': // Restrict filter permissions and handle validation and submission $context = db_fetch_object(db_query("SELECT * FROM {i18n_strings} WHERE lid = %d", $form['lid']['#value'])); $form['i18nstrings_context'] = array('#type' => 'value', '#value' => $context); if ($context && $context->format) { $format = filter_formats($context->format); $disabled = !filter_access($context->format); if ($disabled) { drupal_set_message(t('This string uses the %name input format. You are not allowed to translate or edit texts with this format.', array('%name' => $format->name)), 'warning'); } foreach (element_children($form['translations']) as $langcode) { $form['translations'][$langcode]['#disabled'] = $disabled; } $form['translations']['format_help'] = array( '#type' => 'item', '#title' => t('Input format: @name', array('@name' => $format->name)), '#value' => theme('filter_tips', _filter_tips($context->format, FALSE)) ); $form['submit']['#disabled'] = $disabled; // Replace validate callback $form['#validate'] = array('i18nstrings_translate_edit_form_validate'); } // Aditional submit callback $form['#submit'][] = 'i18nstrings_translate_edit_form_submit'; break; case 'l10n_client_form': $form['#action'] = url('i18nstrings/save'); break; } } /** * Process string editing form validations * * If it is an allowed format, skip default validation, the text will be filtered later */ function i18nstrings_translate_edit_form_validate($form, &$form_state) { $context = $form_state['values']['i18nstrings_context']; if (!filter_access($context->format)) { form_set_error('translations', t('You are not allowed to translate or edit texts with this input format.')); } } /** * Process string editing form submissions. * * Mark translations as current. */ function i18nstrings_translate_edit_form_submit($form, &$form_state) { $lid = $form_state['values']['lid']; foreach ($form_state['values']['translations'] as $key => $value) { if (!empty($value)) { // An update has been made, so we assume the translation is now current. db_query("UPDATE {locales_target} SET status = %d WHERE lid = %d AND language = '%s'", I18NSTRINGS_STATUS_CURRENT, $lid, $key); } } } /** * Get configurable string, * * The difference with i18nstrings() 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. I.e. for the content * types help text (i18ncontent module) there's no way we can override the default (configurable) help text * so what we do is to make it blank in the configuration (so node module doesn't display it) * and then we provide that help text for *all* languages, out from the locales tables. * * As the original language string will be stored in locales too so it should be only used when updating. */ function i18nstrings_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 utility. Marks the translated strings. */ function _i18nstrings($name, $string, $langcode = NULL, $update = FALSE) { $context = i18nstrings_context($name, $string); $context = implode('/', (array)$context); return i18nstrings($name, $string, $langcode, $update) .'[T:'. $string .'('. $context .')]'; } /** * Get translation for user defined string. * * @todo Add support for latest l10n client. * * @param $name * Textgroup and location glued with ':' * @param $string * String in default language * @param $langcode * Language code to get translation for * @param $update * Force update (refresh or create), to be used when updating source strings */ function i18nstrings_translate_string($name, $string, $langcode, $update = FALSE) { global $language; $context = i18nstrings_context($name, $string); if ($update) { i18nstrings_update_string($context, $string); } // Search for existing translation (result will be cached in this function call) $translation = i18nstrings_get_string($context, $langcode); if ($translation === FALSE) { // If the source string is missing, create it anyway. // If $update it should already been created so skip this step. if (!$update) { $source = i18nstrings_get_source($context, $string); if (!$source || empty($source->context)) { i18nstrings_add_string($name, $string); } } $translation = TRUE; } // If current language add to l10n client list for later on page translation. // If language were the default one we are not supossed to reach here. if ($language->language == $langcode && function_exists('l10_client_add_string_to_page')) { // Aditional checking for input format, if its a dangerous one we ignore the string if (empty($source)) { $source = i18nstrings_get_source($context, $string); } if ($source && i18nstrings_allowed_format($source->format) || filter_access($source->format)) { l10_client_add_string_to_page($string, $translation, $source->textgroup); } } return ($translation === TRUE) ? $string : $translation; } /** * Translate object properties. */ function i18nstrings_translate_object($context, &$object, $properties = array(), $langcode = NULL) { global $language; $langcode = $langcode ? $langcode : $language->language; // If language is default, just return. if (language_default('language') != $langcode) { $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_translate_string($context, $object->$property, $langcode); } } } } /** * Update / create object properties */ function i18nstrings_update_object($context, $object, $properties = array()) { $context = i18nstrings_context($context); foreach ($properties as $property) { $context->property = $property; $context->location = i18nstrings_location($context); if (!empty($object->$property)) { i18nstrings_update_string($context, $object->$property); } } } /** * Update / create / remove string * * @param $context * String context. * @pram $string * New value of string for update/create. May be empty for removing. * @param $format * Input format, that must have been checked against allowed formats for translation * @return status * SAVED_UPDATED | SAVED_NEW | SAVED_DELETED */ function i18nstrings_update_string($context, $string, $format = 0) { $context = i18nstrings_context($context, $string, $format); if ($string) { return i18nstrings_add_string($context, $string, $format); } else { return i18nstrings_remove_string($context); } } /** * 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 source string to the locale tables for translation. * * It will also add data into i18n_strings table for faster retrieval and indexing of groups of strings. * Some string context doesn't have a numeric oid (I.e. content types), it will be set to zero. * * This function checks for already existing string without context for this textgroup and updates it accordingly. * It is intended for backwards compatibility, using already created strings. * * @param $name * Textgroup and location glued with ':' * @param $string * Source string (string in default language) * @param $format * Input format, for strings that will go through some filter * @return * Update status. */ function i18nstrings_add_string($name, $string, $format = NULL) { $context = i18nstrings_context($name, $string, $format); $location = i18nstrings_location($context); // Check if we have a source string. $source = i18nstrings_get_source($context, $string); // Default return status if nothing happens $status = -1; // The string may not be allowed for translation depending on its format. if (isset($format) && !i18nstrings_allowed_format($format)) { if ($source) { // The format may have changed and it's not allowed now, delete the source string return i18nstrings_remove_string($context); } else { // We just don't do anything return $status; } } if ($source) { if ($source->source != $string) { // String has changed db_query("UPDATE {locales_source} SET source = '%s', location = '%s' WHERE lid = %d", $string, $location, $source->lid); db_query("UPDATE {locales_target} SET status = %d WHERE lid = %d", I18NSTRINGS_STATUS_UPDATE, $source->lid); $status = SAVED_UPDATED; } elseif ($source->location != $location) { // It's not changed but it didn't have location set db_query("UPDATE {locales_source} SET location = '%s' WHERE lid = %d", $location, $source->lid); $status = SAVED_UPDATED; } // Complete metadata. $context->lid = $source->lid; } else { db_query("INSERT INTO {locales_source} (location, source, textgroup, version) VALUES ('%s', '%s', '%s', '%s')", $location, $string, $context->textgroup, 1); // Mysql just gets last id for latest query $context->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 i18nstrings_save_context($context); return $status; } /** * Check if input format is allowed for translation or whether a textgroup is 'safe' * * @param $format * Input format key or NULL if not format (will be allowed) * @param $textgroup * Check whether strings for this textgroup are allowed when no format information */ function i18nstrings_allowed_format($format = NULL, $textgroup = NULL) { $allowed_formats = variable_get('i18nstrings_allowed_formats', array(variable_get('filter_default_format', 1))); if (isset($format)) { return in_array(filter_resolve_format($format), $allowed_formats); } elseif ($textgroup) { $allowed_groups = variable_get('i18nstrings_allowed_textgroups', array()); return (i18nstrings_group_info($textgroup, 'format') === FALSE) || in_array($textgroup, $allowed_groups); } else { // No format, no textgroup, this is OK return TRUE; } } /** * Save/update context metadata * * There seems to be a race condition sometimes so skip errors, #277711 */ function i18nstrings_save_context($context) { if (db_result(db_query('SELECT lid FROM {i18n_strings} WHERE lid = %d', $context->lid))) { @db_query("UPDATE {i18n_strings} SET type = '%s', objectid = '%s', objectindex = %d, property = '%s', format = %d WHERE lid = %d", $context->type, $context->objectid, (int)$context->objectid, $context->property, $context->format, $context->lid); } else { @db_query("INSERT INTO {i18n_strings} (lid, type, objectid, objectindex, property, format) VALUES(%d, '%s', '%s', %d, '%s', %d)", $context->lid, $context->type, $context->objectid, (int)$context->objectid, $context->property, $context->format); } } /** * 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, i.format 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, i.format 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) { $context = i18nstrings_context($context, $string); 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 i18nstrings() 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', '%s', '%s')", request_uri(), $string, $textgroup, 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. * * Example: * '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) * 'nodetype:type:[type]:name' * 'nodetype:type:[type]:description' * 'profile:category' * 'profile:field:[fid]:title' * * When we don't have 'objectid' or 'property', like for 'profile:category' we need to use * the string itself as a search key, so we store it in $context->source * * If the name has more than 4 elements glued by ':' we add the remaining ones into property * * @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, $format = 0) { // Context may be already an object. if (is_object($context)) { return $context; } else { // Split the name in four parts, remaining elements will be in the last one $parts = split(':', $context); $context = new Stdclass(); $context->textgroup = array_shift($parts); $context->type = array_shift($parts); $context->objectid = $parts ? array_shift($parts) : ''; // Ramaining elements glued again with ':' $context->property = $parts ? implode(':', $parts) : ''; $context->format = $format; $context->location = i18nstrings_location($context); if (!$context->objectid && !$context->property && $string) { $context->source = $string; } return $context; } } /** * Get message parameters from context and string */ function i18nstrings_params($context, $string = NULL) { return array( '%location' => i18nstrings_location($context), '%textgroup' => $context->textgroup, '%string' => $string ? $string : t('[empty string]'), ); } /** * 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 = i18nstrings($name, $string); if ($callback) { $string = $callback($string); } return $string; } /** * Refresh all user defined strings for a given text group * * @param $group * Text group to refresh * @param $delete * Optional, delete existing (but not refresed, strings and translations) * @return Boolean * True if the strings have been refreshed successfully. False otherwise. */ function i18nstrings_refresh_group($group, $delete = FALSE) { // Check for the refresh callback $refresh_callback = i18nstrings_group_info($group, 'refresh callback'); if (!$refresh_callback) { return FALSE; } // Delete data from i18n_strings so it is recreated db_query("DELETE FROM {i18n_strings} WHERE lid IN (SELECT lid FROM {locales_source} WHERE textgroup = '%s')", $group); $result = call_user_func($refresh_callback); // Now delete all source strings that were not refreshed if ($result && $delete) { $result = db_query("SELECT s.* FROM {locales_source} s LEFT JOIN {i18n_strings} i ON s.lid = i.lid WHERE s.textgroup = '%s' AND i.lid IS NULL", $group); while ($source = db_fetch_object($result)) { db_query("DELETE FROM {locales_target} WHERE lid = %d", $source->lid); db_query("DELETE FROM {locales_source} WHERE lid = %d", $source->lid); } } cache_clear_all('locale:'. $group .':', 'cache', TRUE); return $result; } /** * Get refresh callback for a text group * * @param $group * * @return callback */ function i18nstrings_group_info($group = NULL, $property = NULL) { static $info; if (!isset($callbacks)) { $info = module_invoke_all('locale', 'info'); } if ($group && $property) { return isset($info[$group][$property]) ? $info[$group][$property] : NULL; } elseif ($group) { return isset($info[$group]) ? $info[$group] : array(); } else { return $info; } } /*** l10n client related functions ***/ /** * Menu callback. Saves a string translation coming as POST data. */ function i18nstrings_save_string() { global $user, $language; if (user_access('use on-page translation')) { $textgroup = !empty($_POST['textgroup']) ? $_POST['textgroup'] : 'default'; // Default textgroup will be handled by l10n_client module if ($textgroup == 'default') { l10n_client_save_string(); } elseif (isset($_POST['source']) && isset($_POST['target']) && !empty($_POST['form_token']) && drupal_valid_token($_POST['form_token'], 'l10n_client_form')) { i18nstrings_save_translation($language->language, $_POST['source'], $_POST['target'], $textgroup); } } } /** * Import translation for a given textgroup * * @TODO Check string format properly * * This will update multiple strings if there are duplicated ones * * @param $langcode * Language code to import string into. * @param $source * Source string. * @param $translation * Translation to language specified in $langcode. * @param $plid * Optional plural ID to use. * @param $plural * Optional plural value to use. * @return * The number of strings updated */ function i18nstrings_save_translation($langcode, $source, $translation, $textgroup) { include_once 'includes/locale.inc'; $result = db_query("SELECT s.lid, i.format FROM {locales_source} s LEFT JOIN {i18n_strings} i ON s.lid = i.lid WHERE s.source = '%s' AND s.textgroup = '%s'", $source, $textgroup); $count = 0; while ($source = db_fetch_object($result)) { // If we have a format, check format access. Otherwise do regular check. if ($source->format ? filter_access($source->format) : locale_string_is_safe($translation)) { $exists = (bool) db_result(db_query("SELECT lid FROM {locales_target} WHERE lid = %d AND language = '%s'", $source->lid, $langcode)); if (!$exists) { // No translation in this language. db_query("INSERT INTO {locales_target} (lid, language, translation) VALUES (%d, '%s', '%s')", $source->lid, $langcode, $translation); } else { // Translation exists, overwrite db_query("UPDATE {locales_target} SET translation = '%s' WHERE language = '%s' AND lid = %d", $translation, $langcode, $source->lid); } $count ++; } } return $count; } /** * @ingroup i18napi * @{ */ /** * Translate or update user defined string. * * The update option is just vaild for textgroups without input format. * * @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 * DEPRECATED: Use i18nstrings_update() instead. * Whether to force update/create for the string. * * @return $string * Translated string, $string if not found */ function i18nstrings($name, $string, $langcode = NULL) { global $language; $langcode = $langcode ? $langcode : $language->language; // If language is default, just return if (language_default('language') == $langcode) { return $string; } else { return i18nstrings_translate_string($name, $string, $langcode, FALSE); } } /** * Update / create translation source for user defined strings. * * @param $name * Textgroup and location glued with ':'. * @param $string * Source string in default language. Default language may or may not be English. * @param $format * Input format when the string is diplayed through input formats */ function i18nstrings_update($name, $string, $format = NULL) { $context = i18nstrings_context($name, $string, $format); $params = i18nstrings_params($context, $string); if (!i18nstrings_allowed_format($format)) { // This format is not allowed, so we remove the string, in this case we produce a warning drupal_set_message(t('The string %location for textgroup %textgroup is not allowed for translation because of its input format.', $params), 'warning'); return i18nstrings_remove_string($context, $string); } $status = i18nstrings_update_string($context, $string, $format); // Log status message switch ($status) { case SAVED_UPDATED: watchdog('i18nstrings', 'Updated string %location for textgroup %textgroup: %string', $params); break; case SAVED_NEW: watchdog('i18nstrings', 'Created string %location for text group %textgroup: %string', $params); break; } return $status; } /** * Remove source and translations for user defined string. * * Though for most strings the 'name' or 'string id' uniquely identifies that string, * there are some exceptions (like profile categories) for which we need to use the * source string itself as a search key. * * @param $name * Textgroup and location glued with ':'. * @param $string * Optional source string (string in default language). */ function i18nstrings_remove($name, $string = NULL) { $status = i18nstrings_remove_string($name, $string); // Log status message $params = i18nstrings_params($context, $string); switch ($status) { case SAVED_DELETED; watchdog('i18nstrings', 'Deleted string %location for textgroup %textgroup: %string', $params); } return $status; } /** * @} End of "ingroup i18napi". */ /** * Translate configurable string, for plain strings without format. * * Obsoleted, just kept for backwards compatibility * * @see i18nstrings() */ function tt($name, $string, $langcode = NULL, $update = FALSE) { $context = i18nstrings_context($name, $string); // Just update if this textgroup has no input formats, otherwise that module // cannot use this function for updating strings. if ($update && i18nstrings_allowed_format(NULL, $context->textgroup)) { i18nstrings_update_string($name, $string); } return i18nstrings($name, $string, $langcode); } /** * Translate object. * * Obsoleted, just kept for backards compatibility * * @see i18nstrings_translate_object() */ function to($context, $object, $properties = array(), $langcode = NULL, $update = FALSE) { if ($update) { i18nstrings_update_object($context, $object, $properties); } return i18nstrings_translate_object($context, $object, $properties, $langcode); }