'.t('On this page you can refresh and update values for user defined strings.').'
';
$output .= '';
$output .= '- '.t('Use the refresh option when you are missing strings to translate for a given text group. All the strings will be re-created keeping existing translations.').'
';
$output .= '- '.t('Use the update option when some of the strings had been previously translated with the localization system but the translations are not showing up for the configurable strings.').'
';
$output .= '
';
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;
}
/**
* 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);
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);
// 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, 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
* @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, 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, $string);
if (!$context->oid && $context->source) {
// This is a type indexed by string
$context->oid = $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->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;
}