all contrib module security updates done

This commit is contained in:
2020-09-24 22:47:32 +02:00
parent 7d87153100
commit 0fbb9c4610
1612 changed files with 116663 additions and 32269 deletions
@@ -1,6 +1,201 @@
Entity Translation 7.x-1.x, xxxx-xx-xx
--------------------------------------
Entity Translation 7.x-1.1, 2019-11-24
--------------------------------------
#2877103 by stefanos.petrakis, Simon Georges, Delphine Lepers, Andras_Szilagyi,
drasgardian, Alexander Fedorenko, plach: Content can not be created in
another language as the one chosen by configuration
#2580231 by danyg, stefanos.petrakis, MariskaTh: Undefined index in
entity_translation.views.inc
#3085046 by stefanos.petrakis: Fix test breakage due to Title module
7.x-1.0-beta1 version
#3010146 by stefanos.petrakis: Expose extended ET-specific configuration for
pathauto
#3050434 by shital.mahajan, jweowu, stefanos.petrakis: Since PHP 7.0, functions
inspecting arguments, like func_get_args(), no longer report the original
value as passed to a parameter
#3025770 by sudheeshps, patelmayank7552, stefanos.petrakis: PHP 7.0
Compatibility
#2536292 by plopesc, stefanos.petrakis: "Warning: array_merge(): Argument #2 is
not an array in entity_translation_menu_alter" When 'page arguments' or
'access arguments' are not defined in the original item
#2921298 by Stevel, stefanos.petrakis: Missing test dependencies
Entity Translation 7.x-1.0, 2018-04-01
--------------------------------------
(no change)
Entity Translation 7.x-1.0-rc1, 2018-03-21
------------------------------------------
#2927815 by Skabbkladden, dawehner: Infinite loop with language neutral alias
and pathauto.
#2890683 by tobiberlin, stefanos.petrakis: entity_translation_field_attach_form()
adding unwanted fields to form.
#2907366 by swati.nuna: Invalid hook hook_translation_info() mentioned in
docblock for entity_translation_edit_form_info().
#1799770 by plach, bforchhammer: Update id and bundle when setting a wrapped
entity.
#2339315 by plach: Added documentation for
hook_entity_translation_source_field_state_alter().
#1506054 by plach, Owen Barton: Taxonomy term reference: language-aware widget
and autocomplete.
#2907275 by jalpesh: CSS class name misspelled in entity_translation_overview().
Entity Translation 7.x-1.0-beta7, 2017-08-21
--------------------------------------------
#2903195 by stefanos.petrakis, BAHbKA, plach: Enabling translation for not
empty taxonomy vocabulary.
#2425825 by jamesrward: Update from before beta4 fails.
#2900094 by joseph.olstad: code sniffer drupal.org recommended code syntax
fixes.
#2883805 by mikran: entity_translation_update_7008 is broken when taxonomy
module is not enabled.
#2877074 by plach, stefanos.petrakis, joseph.olstad: Refactor the
entity_translation_language() callback to make it bundle-specific.
#2743685 by stefanos.petrakis, bwaindwain: Pathauto update for all translations
for a single node.
#2899658 by joseph.olstad, stefanos.petrakis: entity_translation_upgrade_do
needs to automatically create the entity_translation row for the source
language in entity_translation.
#1800158 by meichr: Entity Translation Upgrade, drush extension.
#2885858 by StephaneQ: Can't delete taxonomy term translation since beta6
update.
#2572203 by mattew: Wrong query generated when several Translations
relationships are used.
#2824255 by ccarrascal, plach: Notice: Undefined index for language in
setTranslation().
#2750179 by wadmiraal: Allowed default translation handler to be completely
overridden.
#2307937 by meichr, joseph.olstad: Batch API needs $context['results'] handled
as an array, not a value.
#1175170 by hgoto, Pisco, alberto56: Optionally enable disabled languages for
entity translation.
#2870524 by stefanos.petrakis, joseph.olstad: Field copy fails with content
translation for fields with entity translation enabled.
Entity Translation 7.x-1.0-beta6, 2017-03-03
--------------------------------------------
#2849464 by plach, czigor, stefanos.petrakis: Added an API to set the active
language.
#2438615 by David_Rothstein, matthiasm11: New nodes are always created using
LANGUAGE_NONE (only changed to the correct language during form submission).
#2820454 by Dylan Donkersgoed: PHP notice when attaching single field
with field_attach_form.
#2826297 by Thomas Cys, dwebpoint, stefanos.petrakis: 'clone' is a reserved
keyword introduced in PHP version 5.0 and cannot be invoked as a function.
#2189567 by stefanos.petrakis, jmuzz: Fixed problems enabling and disabling
translation on a field with revisions enabled.
#2798721 by gnucifer: Path scheme not initialized on delete translation
confirmation form.
#2215771 by colan, stefanos.petrakis: Fixed "Undefined index: translate in
EntityTranslationDefaultHandler->entityForm()".
#1829636 by bforchhammer, stefanos.petrakis: Fixed "All languages" suffix not
working nicely with multi-value fields.
#2746045 by fengtan: Converted dsm calls to watchdog calls for path scheme
definition warnings.
#2757601 by ctrnz, aks22: Fixed entity_translation_update_7009() assumes
i18n_taxonomy is enabled.
#1172104 by james.williams, daro.pl: Improved description of the language
fallback option.
#2734373 by plach, consulio: "entity_translation_update_7008" is broken when no
term is translated.
#2734301 by plach, sinasalek: Fixed Numeric value out of range: 1264 Out of
range value for column 'i18n_mode'.
#1661348 by plach, badrange, colan, candelas: Added "I18n Taxonomy" integration.
Entity Translation 7.x-1.0-beta5, 2016-05-08
--------------------------------------------
#929402 by das-peter, bastnic, kristiaanvandeneynde, brockfanning, greenjuls,
make77, steinmb: Added support for migrate module.
#1291388 by Jax, poiu, plach: Added support for multilingual core search.
#2339315 by loopduplicate, gnucifer, plopesc, james.williams: Fixed Source
language prepopulated values should be also included in $form_state['field'].
#2648062 by brunoric, cebasqueira, lucasr, mmchristoph: Fixed Fatal error: Call
to a member function getFormLanguage() on a non-object.
#S156840 by HelenaEksler, plach: Fixed XSS vulnerability in the translation
form page title.
#2444203 by Leksat, badrange, attiks: Show fallback information on the
translation overview.
#2458393 by catch, plach, douggreen: Fixed unindexed query in
EntityTranslationDefaultHandler::loadMultiple().
#2462909 by GeduR: Misspelling translation exists help in views handler
definition.
#2166157 by rsmylski, ciss: entity translation handler id for new entities not
unique enough - can cause infinite/circular child reference.
#2452279 by GeduR: Duplicated variable $language in
entity_translation_edit_access().
#2423661 by marcoscano: Module api documentation has wrong example ("path sets"
instead of "path schemes").
#2305547 by das-peter: Fixed undefined index name.
#1989084 by jsacksick, candelas: Fixed <nolink> and validation.
#2203801 by idflood: Impossible to update taxonomy term after enabling
i18n_taxonomy.
#2415189 by catch: Translation deletion bypasses entity saving.
Entity Translation 7.x-1.0-beta4, 2015-01-23
--------------------------------------------
#2396103 by plach: Fixed [DATA LOSS] Translations deleted.
#2218087 by plach: Fixed PHP Fatal error: Unsupported operand types in
modules/field_ui/field_ui.admin.inc on line 477.
#2396653 by SchnWalter, mongolito404: Fix validation handler for the entity
language widget.
#2395327 by damiankloip: FATAL error when requesting an edit path for a
non-existent entity.
#2342787 by jcisio: Language widget submit handler runs on disabled entity
types.
#2389945 by rafal.enden: Tests don't pass when using different source than
original node language.
#2185523 by leon.nk: Document that entity_translation_enabled_bundle() does not
check whether the entity type is translatable.
#2378195 by Johnny vd Laar: Node edit notice when author of a translation is
deleted.
#2382713 by seanr: entity_translation_update_7006 fails because of SQL error
before variable_set('entity_translation_revision_enabled', FALSE); can run.
#1046282 by plach, good_man, miro_dietiker, Gábor Hojtsy: Make the module work
with revisions.
#2179183 by dkingofpa, Jānis Bebrītis | SocialNicheGuru: Fixed Fatal error:
Unsupported operand types in /entity_translation.module on line 188.
#1605406 by joelrosen, Jose Reyero, paulihuhtiniemi, guillaumev | mindaugasd:
Exposed translated field content to Views so that multiple language
translations can be shown simultaneously.
#2160559 by w3wfr: Warning because of empty value line 675.
#2151503 by SebCorbin: Check for translation access for Add links.
#2130091 by peximo: Media edit dialog integration bug.
#2065221 by catch, plach: Fixed Static cache for handlers should key by $vid.
#1516202 by fabsor, spotzero: Added a CTools access plugin for checking if a
translation is available.
#2027513 by GaëlG, jcisio: Added support for nested values for 'edit form' in
hook_entity_info().
#2073231 by plach: Rename entity_translation_form_language() to
entity_translation_get_existing_language().
#2072865 by douggreen: Fixed Only replace 'delete' button with
'delete translation', don't add one that doesn't exist.
#1865244 by plach | bojanz, agoradesign, ciss, interdruper, joel_osc: Added
support for multiple translation handlers on the same form.
#2055491 by plach: Fixed Synced image fields cannot be emptied.
#1866076 by grndlvl: Added support for non-entity load tokens when declaring
translations.
Entity Translation 7.x-1.0-beta3, 2013-07-23
--------------------------------------------
#2037789 by Jelle_S: Fixed Default to user_access() access callback like drupal
menu does.
#1982140 by bforchhammer | carligraph: Fixed Menu item is deleted when adding a
third translation.
#2000926 by GiorgosK, bforchhammer: Fixed entity_translation_i18n_menu should
check if entity is node before deleting menu links.
#1991452 by twistor: Fixed Strict warnings on testing page.
#1992588 by bforchhammer: Fixed PHP notices on node page (menu-related).
#1979406 by milesw: Field values for node translations not filled correctly when
making a field translatable.
#1969366 by plach | EdNet: Fixed unsupported operand types in
@@ -21,7 +216,7 @@ Entity Translation 7.x-1.x, xxxx-xx-xx
#1947764 by plach: Improve bundle translatability checks.
#1370900 by mojzis, plach | mgladding: Fixed Fatal Error: Cannot access empty
property in translation.handler.inc.
Issue #1924088 by plach | drzraf: Fixed Wrong form 'language' value in case of
#1924088 by plach | drzraf: Fixed Wrong form 'language' value in case of
content-translation enabled bundles.
#1933742 by das-peter: Fixed Delete translation doesn't flush the entity cache.
#1903024 by fabsor | dimitrileonidas: Fixed SQL error in Views when adding an
@@ -223,7 +418,7 @@ Entity Translation 7.x-1.0-alpha1, 2011-09-07
editing workflow.
#1031370 by joostvdl | rfay: Fixed Translation publishing status is hidden under
collapsed 'publishing options' fieldset (and is hidden by default).
#1003876 by plach, good_man: (follow-up) Fixed original label translation.
#1003876 by plach, good_man: (follow-up) Fixed original label translation.
#1003876 by good_man, plach: Introduced support for translated entity labels.
#936646 by good_man, fietserwin, plach, klonos: Fixed Undefined index access
callback/arguments in entity_translation_menu().
@@ -9,8 +9,9 @@ Project maintainers
The Entity translation maintainers oversee the development of the project as a
whole. The project maintainers for Entity translation are:
- Francesco Placella 'plach' <http://drupal.org/user/183211>
- Daniel F. Kudwien 'sun' <http://drupal.org/user/54136>
- Francesco Placella 'plach' <https://www.drupal.org/u/plach>
- Daniel F. Kudwien 'sun' <https://www.drupal.org/u/sun>
- Stefanos Petrakis 'stefanos.petrakis' <https://www.drupal.org/u/stefanospetrakis>
Component maintainers
@@ -25,14 +26,15 @@ http://drupal.org/node/363367 to find out how to become a component maintainer.
Current component maintainers for Entity translation:
Base system
- Francesco Placella 'plach' <http://drupal.org/user/183211>
- Benedikt Forchhammer 'bforchhammer' <http://drupal.org/user/216396>
- Francesco Placella 'plach' <https://www.drupal.org/u/plach>
- Benedikt Forchhammer 'bforchhammer' <https://www.drupal.org/u/bforchhammer>
- Stefanos Petrakis 'stefanos.petrakis' <https://www.drupal.org/u/stefanospetrakis>
Menu integration
- Benedikt Forchhammer 'bforchhammer' <http://drupal.org/user/216396>
- Benedikt Forchhammer 'bforchhammer' <https://www.drupal.org/u/bforchhammer>
Views integration
- Fabian Sörqvist 'fabsor' <http://drupal.org/user/255704>
- Fabian Sörqvist 'fabsor' <https://www.drupal.org/u/fabsor>
Node translation upgrade
- Francesco Placella 'plach' <http://drupal.org/user/183211>
- Francesco Placella 'plach' <https://www.drupal.org/u/plach>
@@ -29,9 +29,50 @@ None.
* @todo
-- DEVELOPERS --
--- PHP7 Compatibility detection ---
Use PHP Code Sniffer (via coder) with the PHPCompatibility sniffs installed.
* https://www.drupal.org/project/coder
* https://github.com/PHPCompatibility/PHPCompatibility
Once phpcs is installed, a simple way to add PHPCompatibility is:
* Run 'phpcs -i' to confirm the current recognised coding standards.
Assuming that PHPCompatibility is not already listed...
* Run 'phpcs --config-show' and take note of the "installed_paths" value.
* Run 'composer require phpcompatibility/php-compatibility'
* Establish the absolute /path/to/vendor/phpcompatibility/php-compatibility
which you should now have somewhere.
* Run 'phpcs --config-set installed_paths OLD,NEW' where OLD is the original
value, and NEW is the php-compatibility path.
* Run 'phpcs -i' to confirm that PHPCompatibility has been added to the list
of coding standards.
* Run 'phpcs --standard=PHPCompatibility <files>' to check <files>
--- PHP7 Compatibility hints ---
func_get_args() should be placed at the beginning of a method or function,
before any modifications to the function's arguments take place. Its value
should be assigned to a locally scoped variable - usually named $args by
convention - to be used inside the function as needed. This is due to
changes in PHP 7.0 and onwards.
@see http://php.net/manual/en/function.func-get-args.php#refsect1-function.func-get-args-notes
-- CONTACT --
Current maintainers:
* Francesco Placella (plach) - http://drupal.org/user/183211
* Daniel F. Kudwien (sun) - http://drupal.org/user/54136
* Stefanos Petrakis (stefanos.petrakis) - https://www.drupal.org/u/stefanospetrakis
@@ -13,11 +13,30 @@ function entity_translation_admin_form($form, $form_state) {
$form['locale_field_language_fallback'] = array(
'#type' => 'checkbox',
'#title' => t('Enable language fallback'),
'#description' => t('When language fallback is enabled, if a translation is not available for the requested language an existing one will be displayed.'),
'#title' => t('Allow language fallback'),
'#description' => t('When language fallback is allowed, an alternative translation will be displayed if the requested language is not available. Other modules, such as <a href="https://www.drupal.org/project/language_hierarchy" target="_blank">Language Hierarchy</a>, are needed to manage which languages are used as fallbacks. Otherwise, existing translations will be chosen based on their <a href="!url">language weight</a>.', array('!url' => url('admin/config/regional/language'))),
'#default_value' => variable_get('locale_field_language_fallback', TRUE),
);
$form['entity_translation_languages_enabled'] = array(
'#type' => 'checkbox',
'#title' => t('Use only enabled languages'),
'#description' => t('If checked, disabled languages will not show up as available languages. This setting does not apply to content types that are configured to use the Multilingual content (i18n_node) module as this module provides its own configuration.'),
'#default_value' => variable_get('entity_translation_languages_enabled', FALSE),
);
$form['entity_translation_show_fallback_on_overview_pages'] = array(
'#type' => 'checkbox',
'#title' => t('Show fallback statuses on overview pages'),
'#description' => t('Enable to the show fallback information on the entity overview pages.'),
'#default_value' => variable_get('entity_translation_show_fallback_on_overview_pages', FALSE),
'#states' => array(
'visible' => array(
':input[name="locale_field_language_fallback"]' => array('checked' => TRUE),
),
),
);
$form['entity_translation_shared_labels'] = array(
'#type' => 'checkbox',
'#title' => t('Display shared labels'),
@@ -33,7 +52,7 @@ function entity_translation_admin_form($form, $form_state) {
);
$_null = NULL;
list($items, ) = menu_router_build();
list($items,) = menu_router_build();
_entity_translation_validate_path_schemes($_null, FALSE, $items);
foreach (entity_get_info() as $entity_type => $info) {
@@ -153,6 +172,46 @@ function entity_translation_admin_form($form, $form_state) {
}
}
if (module_exists('pathauto')) {
$form['pathauto'] = array(
'#type' => 'fieldset',
'#title' => t('Pathauto'),
'#collapsible' => FALSE,
);
$form['pathauto']['entity_translation_pathauto_state_active_new_translation'] = array(
'#title' => t('Pathauto state automatically activated on forms for new translations'),
'#type' => 'checkbox',
'#description' => t('When this option is enabled, the Pathauto state checkbox is automatically selected on the add-translation form. <br/><b>Attention:</b>When combining this option with the next one it may lead to cases where all aliases for all languages are auto-updated whenever a new translation is added.'),
'#default_value' => variable_get('entity_translation_pathauto_state_active_new_translation', TRUE),
);
// These are the possible options when pathauto state is active.
$mode_options = array(
'generate_alias_active_language' => t('Generate alias only for current language'),
'generate_missing_aliases_all_languages' => t('Generate missing aliases for all languages'),
'generate_all_aliases_all_languages' => t('Generate all aliases for all languages'),
);
// Single entity update mode.
$form['pathauto']['entity_translation_pathauto_state_mode_update'] = array(
'#title' => t('Pathauto state mode for single update'),
'#type' => 'select',
'#description' => t('When the Pathauto state is activated, the mode defined here will be used for alias generation, on a single update, e.g. when editing a node using the node edit form.'),
'#options' => $mode_options,
'#default_value' => variable_get('entity_translation_pathauto_state_mode_update', 'generate_alias_active_language'),
);
// Bulk entity updates mode.
$form['pathauto']['entity_translation_pathauto_state_mode_bulkupdate'] = array(
'#title' => t('Pathauto state mode for bulk updates'),
'#type' => 'select',
'#description' => t('When the Pathauto state is activated, the mode defined here will be used for alias generation, on bulk updates.'),
'#options' => $mode_options,
'#default_value' => variable_get('entity_translation_pathauto_state_mode_bulkupdate', 'generate_all_aliases_all_languages'),
);
}
$form = system_settings_form($form);
// Menu rebuilding needs to be performed after the system settings are saved.
@@ -198,12 +257,17 @@ function entity_translation_settings_init($entity_type, $settings = array()) {
* Translations overview page callback.
*/
function entity_translation_overview($entity_type, $entity, $callback = NULL) {
$args = func_get_args();
// Entity translation and node translation share the same system path.
if ($callback && entity_translation_node($entity_type, $entity)) {
return entity_translation_overview_callback($callback, $entity);
if ($callback && !entity_translation_enabled($entity_type, $entity)) {
$callback_args = array_slice($args, 3);
return entity_translation_overview_callback($callback, $callback_args);
}
$handler = entity_translation_get_handler($entity_type, $entity);
// Ensure $entity holds an entity object and not an id.
$entity = $handler->getEntity();
$handler->initPathScheme();
// Initialize translations if they are empty.
@@ -217,8 +281,7 @@ function entity_translation_overview($entity_type, $entity, $callback = NULL) {
// languages.
if ($handler->initOriginalTranslation()) {
// FIXME!
field_attach_presave($entity_type, $entity);
field_attach_update($entity_type, $entity);
entity_translation_entity_save($entity_type, $entity);
}
$header = array(t('Language'), t('Source language'), t('Translation'), t('Status'), t('Operations'));
@@ -234,6 +297,8 @@ function entity_translation_overview($entity_type, $entity, $callback = NULL) {
$links = EntityTranslationDefaultHandler::languageSwitchLinks($path);
}
$show_fallback = variable_get('locale_field_language_fallback', TRUE) && variable_get('entity_translation_show_fallback_on_overview_pages', FALSE);
foreach ($languages as $language) {
$classes = array();
$options = array();
@@ -284,7 +349,7 @@ function entity_translation_overview($entity_type, $entity, $callback = NULL) {
// No such translation in the set yet: help user to create it.
$row_title = $source_name = t('n/a');
if ($source != $langcode && $handler->getAccess('update')) {
if ($source != $langcode && $handler->getAccess('update') && $handler->getTranslationAccess($langcode)) {
list(, , $bundle) = entity_extract_ids($entity_type, $entity);
$translatable = FALSE;
@@ -300,9 +365,21 @@ function entity_translation_overview($entity_type, $entity, $callback = NULL) {
$link = isset($add_links->links[$langcode]['href']) ? $add_links->links[$langcode] : array('href' => $add_path, 'language' => $language);
$link['query'] = isset($_GET['destination']) ? drupal_get_destination() : FALSE;
$options[] = $translatable ? l(t('add'), $link['href'], $link) : t('No translatable fields');
$classes[] = $translatable ? '' : 'non-traslatable';
$classes[] = $translatable ? '' : 'non-translatable';
}
$status = t('Not translated');
// Show fallback information if required.
if ($show_fallback) {
$language_fallback_candidates = _entity_translation_language_fallback_get_candidates($language);
$fallback_candidates = array_intersect_key(drupal_map_assoc($language_fallback_candidates), $translations->data);
$fallback_langcode = reset($fallback_candidates);
if ($fallback_langcode !== FALSE) {
$status = t('Fallback from @language', array('@language' => $languages[$fallback_langcode]->name));
$label = _entity_translation_label($entity_type, $entity, $fallback_langcode);
$link = isset($links->links[$langcode]['href']) ? $links->links[$langcode] : array('href' => $path, 'language' => $langcode);
$row_title = l($label, $link['href'], $link);
}
}
}
$rows[] = array(
'data' => array($language_name, $source_name, $row_title, $status, implode(" | ", $options)),
@@ -329,13 +406,13 @@ function entity_translation_overview($entity_type, $entity, $callback = NULL) {
/**
* Calls the appropriate translation overview callback.
*/
function entity_translation_overview_callback($callback, $entity) {
function entity_translation_overview_callback($callback, $args) {
if (module_exists($callback['module'])) {
if (isset($callback['file'])) {
$path = isset($callback['file path']) ? $callback['file path'] : drupal_get_path('module', $callback['module']);
require_once DRUPAL_ROOT . '/' . $path . '/' . $callback['file'];
}
return $callback['page callback']($entity);
return call_user_func_array($callback['page callback'], $args);
}
}
@@ -361,7 +438,7 @@ function _entity_translation_label($entity_type, $entity, $langcode = NULL) {
/**
* Theme wrapper for the entity translation language page.
*/
function theme_entity_translation_overview($variables){
function theme_entity_translation_overview($variables) {
$rows = $variables['rows'];
return theme('table', array(
'rows' => $rows,
@@ -372,7 +449,7 @@ function theme_entity_translation_overview($variables){
/**
* Theme wrapper for the entity translation language outdated translation.
*/
function theme_entity_translation_overview_outdated($variables){
function theme_entity_translation_overview_outdated($variables) {
$message = $variables['message'];
return ' - <span class="marker">' . $message . '</span>';
}
@@ -382,12 +459,15 @@ function theme_entity_translation_overview_outdated($variables){
*/
function entity_translation_delete_confirm($form, $form_state, $entity_type, $entity, $langcode) {
$handler = entity_translation_get_handler($entity_type, $entity);
$handler->setActiveLanguage($langcode);
$handler->initPathScheme();
$languages = language_list();
$form = array(
'#handler' => $handler,
'#entity_type' => $entity_type,
'#entity' => $entity,
// Ensure $entity holds an entity object and not an id.
'#entity' => $handler->getEntity(),
'#language' => $langcode,
);
@@ -416,13 +496,12 @@ function entity_translation_delete_confirm_submit($form, &$form_state) {
// Remove the translation entry and the related fields.
$handler->removeTranslation($langcode);
field_attach_presave($entity_type, $entity);
field_attach_update($entity_type, $entity);
entity_translation_entity_save($entity_type, $entity);
$form_state['redirect'] = $handler->getTranslatePath();
}
/*
/**
* Confirm form for changing field translatability.
*/
function entity_translation_translatable_form($form, &$form_state, $field_name) {
@@ -517,7 +596,7 @@ function entity_translation_translatable_form_submit($form, $form_state) {
batch_set($batch);
}
/*
/**
* Toggle translatability of the given field.
*
* This is called from a batch operation, but should only run once per field.
@@ -548,6 +627,7 @@ function entity_translation_translatable_batch($translatable, $field_name, $copy
$query = new EntityFieldQuery();
$count = $query
->fieldCondition($field_name)
->age(FIELD_LOAD_REVISION)
->count()
->execute();
@@ -567,11 +647,32 @@ function entity_translation_translatable_batch($translatable, $field_name, $copy
$result = $query
->fieldCondition($field_name)
->entityOrderBy('entity_id')
->age(FIELD_LOAD_REVISION)
->range($offset, $limit)
->execute();
foreach ($result as $entity_type => $entities) {
foreach (entity_load($entity_type, array_keys($entities)) as $entity) {
foreach ($result as $entity_type => $partial_entities) {
// Load all revisionable entities' revisions.
if (EntityTranslationDefaultHandler::isEntityTypeRevisionable($entity_type)) {
$entities = array();
// Extract the revision identifier from the entity's definition.
$entity_info = entity_get_info($entity_type);
$revision_id_key = $entity_info['entity keys']['revision'];
// Load each revisionable entity's revision using $conditions, which
// should include the revision id information.
foreach ($partial_entities as $partial_entity) {
$conditions = (array) $partial_entity;
$revision_condition = array_intersect_key($conditions, array($revision_id_key => $revision_id_key));
$entity_revisions = entity_load($entity_type, FALSE, $revision_condition);
$entities[] = reset($entity_revisions);
}
}
else {
$entities = entity_load($entity_type, array_keys($partial_entities));
}
foreach ($entities as $entity) {
$context['sandbox']['progress']++;
$handler = entity_translation_get_handler($entity_type, $entity);
$langcode = $handler->getLanguage();
@@ -590,9 +691,10 @@ function entity_translation_translatable_batch($translatable, $field_name, $copy
if ($translatable && isset($entity->{$field_name}[LANGUAGE_NONE])) {
// If the field is being switched to translatable and has data for
// LANGUAGE_NONE then we need to move the data to the right language.
// In case the 'Copy translations' option was selected, move the
// available LANGUAGE_NONE field data into all existing translation
// languages, otherwise only into the entity's language.
$translations = $handler->getTranslations();
if ($copy_all_languages && !empty($translations->data)) {
foreach ($translations->data as $translation) {
$entity->{$field_name}[$translation['language']] = $entity->{$field_name}[LANGUAGE_NONE];
@@ -650,8 +752,7 @@ function _entity_translation_update_field($entity_type, $entity, $field_name) {
// otherwise any stored empty field value would be deleted. If this happens
// the range queries would be messed up.
if ($empty < count($entity->{$field_name})) {
field_attach_presave($entity_type, $entity);
field_attach_update($entity_type, $entity);
entity_translation_entity_save($entity_type, $entity);
}
}
@@ -667,3 +768,36 @@ function entity_translation_translatable_batch_done($success, $results, $operati
drupal_set_message(t("Something went wrong while processing data. Some nodes may appear to have lost fields."));
}
}
/**
* Returns language fallback candidates for a certain language.
*
* @param object $language
* Drupal language object.
*
* @return array
* An array of language codes in the fallback order.
*/
function _entity_translation_language_fallback_get_candidates($language) {
// Save original fallback candidates.
$language_fallback_original = drupal_static('language_fallback_get_candidates');
// Replace all global languages with the given one. We need this because the
// language_fallback_get_candidates() does not take the $language parameter,
// however other modules (like language_fallback) may use current language(s)
// when they alter the language fallback candidates.
$languages_original = array();
foreach (language_types() as $language_type) {
$languages_original[$language_type] = $GLOBALS[$language_type];
$GLOBALS[$language_type] = $language;
}
// Clear static cache, so that fallback candidates are recalculated.
drupal_static_reset('language_fallback_get_candidates');
$language_fallback_candidates = language_fallback_get_candidates();
// Restore original data.
$language_fallback =& drupal_static('language_fallback_get_candidates');
$language_fallback = $language_fallback_original;
foreach ($languages_original as $language_type => $_language) {
$GLOBALS[$language_type] = $_language;
}
return $language_fallback_candidates;
}
@@ -64,7 +64,7 @@
* - skip original values access: A flag specifying whether skipping access
* control when editing original values for this entity. Defaults to FALSE.
* - bundle callback: A callback to check whether the passed bundle has entity
* translation enabled. If empty all bundles are supposed to be enabled.
* translation enabled. Defaults to TRUE for all bundles.
* - default settings: The defaults to be applied to settings when an explicit
* choice is missing.
*/
@@ -91,7 +91,7 @@ function hook_entity_info() {
'translation' => array(
'entity_translation' => array(
'class' => 'EntityTranslationCustomEntityHandler',
'path sets' => array(
'path schemes' => array(
'default' => array(
'base path' => 'custom_entity_2/%custom_entity',
'path wildcard' => '%custom_entity',
@@ -151,3 +151,30 @@ function hook_entity_translation_update($entity_type, $entity, $translation, $va
*/
function hook_entity_translation_delete($entity_type, $entity, $langcode) {
}
/**
* Allows modules to act when a revision translation is deleted.
*
* @param $entity_type
* The entity type.
* @param $entity
* The entity.
* @param $langcode
* The langcode of the revision translation which was deleted.
*/
function hook_entity_translation_delete_revision($entity_type, $entity, $langcode) {
}
/**
* Allows to sets the right values in the form state when adding a translation.
*/
function hook_entity_translation_source_field_state_alter(&$field_state) {
if (isset($field_state['entity'])) {
module_load_include('inc', 'entity', 'includes/entity.ui');
foreach ($field_state['entity'] as $delta => $entity) {
if ($entity instanceof FieldCollectionItemEntity) {
$field_state['entity'][$delta] = entity_ui_clone_entity('field_collection_item', $entity);
}
}
}
}
@@ -5,11 +5,18 @@ core = 7.x
configure = admin/config/regional/entity_translation
dependencies[] = locale (>7.14)
test_dependencies[] = pathauto:pathauto
; The version restriction in the following line could be
; removed as soon as [#3085061] is fixed.
test_dependencies[] = title:title (=7.x-1.0-alpha9)
files[] = includes/translation.handler_factory.inc
files[] = includes/translation.handler.inc
files[] = includes/translation.handler.comment.inc
files[] = includes/translation.handler.node.inc
files[] = includes/translation.handler.taxonomy_term.inc
files[] = includes/translation.handler.user.inc
files[] = includes/translation.migrate.inc
files[] = tests/entity_translation.test
@@ -19,10 +26,10 @@ files[] = views/entity_translation_handler_field_label.inc
files[] = views/entity_translation_handler_filter_entity_type.inc
files[] = views/entity_translation_handler_filter_language.inc
files[] = views/entity_translation_handler_filter_translation_exists.inc
files[] = views/entity_translation_handler_field_field.inc
; Information added by drupal.org packaging script on 2013-04-26
version = "7.x-1.0-beta2+33-dev"
; Information added by Drupal.org packaging script on 2019-11-24
version = "7.x-1.1"
core = "7.x"
project = "entity_translation"
datestamp = "1366981073"
datestamp = "1574630892"
@@ -9,6 +9,8 @@
* Implements hook_schema().
*/
function entity_translation_schema() {
$schema = array();
$schema['entity_translation'] = array(
'description' => 'Table to track entity translations',
'fields' => array(
@@ -25,7 +27,12 @@ function entity_translation_schema() {
'not null' => TRUE,
'description' => 'The entity id this translation relates to',
),
// @todo: Consider an integer field for 'language'.
'revision_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The entity revision id this translation relates to',
),
'language' => array(
'type' => 'varchar',
'length' => 32,
@@ -73,6 +80,78 @@ function entity_translation_schema() {
),
'primary key' => array('entity_type', 'entity_id', 'language'),
);
$schema['entity_translation_revision'] = array(
'description' => 'Table to track entity translation revisions',
'fields' => array(
'entity_type' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'The entity type this translation revision relates to',
),
'entity_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The entity id this translation revision relates to',
),
'revision_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The entity revision id this translation relates to',
),
'language' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
'description' => 'The target language for this translation revision.',
),
'source' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
'description' => 'The source language from which this translation revision was created.',
),
'uid' => array(
'description' => 'The author of this translation revision.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'status' => array(
'description' => 'Boolean indicating whether the translation revision is published (visible to non-administrators).',
'type' => 'int',
'not null' => TRUE,
'default' => 1,
),
'translate' => array(
'description' => 'A boolean indicating whether this translation revision needs to be updated.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'created' => array(
'description' => 'The Unix timestamp when the translation revision was created.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'changed' => array(
'description' => 'The Unix timestamp when the translation revision was most recently saved.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
'primary key' => array('entity_type', 'revision_id', 'language'),
'indexes'=> array('revision_id' => array('revision_id')),
);
return $schema;
}
@@ -93,6 +172,12 @@ function entity_translation_install() {
// Make translation use the content language type.
variable_set('translation_language_type', LANGUAGE_TYPE_CONTENT);
// Enable revision support for entity translation.
variable_set('entity_translation_revision_enabled', TRUE);
// Enable taxonomy autocomplete support.
variable_set('entity_translation_taxonomy_autocomplete', TRUE);
}
/**
@@ -254,3 +339,162 @@ function entity_translation_update_7003() {
function entity_translation_update_7004() {
entity_info_cache_clear();
}
/**
* Rebuild the class registry to pick up the translation handler factory class.
*/
function entity_translation_update_7005() {
registry_rebuild();
}
/**
* Add revision schema for entity translation metadata.
*/
function entity_translation_update_7006() {
// Create revision id column.
$spec = array(
'type' => 'int',
'unsigned' => TRUE,
// If we have existing data we cannot enforce this to be NOT NULL.
'not null' => FALSE,
'description' => 'The entity revision id this translation relates to',
);
// Create revision id column if it doesn't exist already.
if (!db_field_exists('entity_translation', 'revision_id')) {
db_add_field('entity_translation', 'revision_id', $spec);
}
// Create the entity translation revision schema.
$table = array(
'description' => 'Table to track entity translation revisions',
'fields' => array(
'entity_type' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'The entity type this translation revision relates to',
),
'entity_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The entity id this translation revision relates to',
),
'revision_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The entity revision id this translation relates to',
),
'language' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
'description' => 'The target language for this translation revision.',
),
'source' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
'description' => 'The source language from which this translation revision was created.',
),
'uid' => array(
'description' => 'The author of this translation revision.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'status' => array(
'description' => 'Boolean indicating whether the translation revision is published (visible to non-administrators).',
'type' => 'int',
'not null' => TRUE,
'default' => 1,
),
'translate' => array(
'description' => 'A boolean indicating whether this translation revision needs to be updated.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'created' => array(
'description' => 'The Unix timestamp when the translation revision was created.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'changed' => array(
'description' => 'The Unix timestamp when the translation revision was most recently saved.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
'primary key' => array('entity_type', 'revision_id', 'language'),
'indexes'=> array('revision_id' => array('revision_id')),
);
// Create entity translation revision table if it doesn't exist already.
if (!db_table_exists('entity_translation_revision')) {
db_create_table('entity_translation_revision', $table);
}
}
/**
* Disable revision support on existing installations.
*/
function entity_translation_update_7007() {
// Revision support is not enabled by default on existing installations as
// making it work implies copying translation metadata to the
// {entity_translation_revision} table for all the existing translations.
// Since this process cannot be reliably implemented in an update function,
// we leave the choice of manually performing the upgrade to people with the
// required skills to do so. Be aware that enabling revision support on sites
// where data has not been manually migrated may cause translation metadata to
// be permanently lost or corrupted. See https://www.drupal.org/node/2396103.
variable_set('entity_translation_revision_enabled', FALSE);
}
/**
* Enable Entity Translation for taxonomy vocabularies having terms translated
* through it.
*/
function entity_translation_update_7008() {
if (!module_exists('taxonomy')) {
return;
}
// According to EXPLAIN joining {taxonomy_vocabulary} here makes the query
// perform way worse, so we just split into two quick ones.
$query = "SELECT t.vid
FROM {entity_translation} et
JOIN {taxonomy_term_data} t ON et.entity_type = 'taxonomy_term' AND et.entity_id = t.tid AND et.source != ''
GROUP BY t.vid";
$vids = db_query($query)->fetchCol();
if ($vids) {
$query = "SELECT v.machine_name FROM {taxonomy_vocabulary} v WHERE v.vid IN (:vids)";
$names = db_query($query, array(':vids' => $vids))->fetchCol();
$info = variable_get('entity_translation_taxonomy', array());
foreach ($names as $name) {
$info[$name] = TRUE;
}
variable_set('entity_translation_taxonomy', $info);
}
}
/**
* Make sure "i18n_mode" is correctly set for vocabularies having entity
* translation enabled.
*/
function entity_translation_update_7009() {
$info = array_filter(variable_get('entity_translation_taxonomy', array()));
if ($info && module_exists('i18n_taxonomy')) {
$query = "UPDATE {taxonomy_vocabulary} SET i18n_mode = :et_mode WHERE machine_name IN (:names)";
$names = array_keys(array_filter($info));
db_query($query, array(':et_mode' => 32768, ':names' => $names));
}
}
@@ -0,0 +1,17 @@
<?php
/**
* @file
* Implements a Migrate destination handler to automatically add translations
* to entities with translatable fields.
*/
/**
* Implements hook_migrate_api().
*/
function entity_translation_migrate_api() {
$api = array(
'api' => 2,
'destination handlers' => array('MigrateTranslationEntityHandler'),
);
return $api;
}
@@ -6,6 +6,7 @@
*/
module_load_include('inc', 'entity_translation', 'entity_translation.node');
module_load_include('inc', 'entity_translation', 'entity_translation.taxonomy');
/**
@@ -23,6 +24,11 @@ define('ENTITY_TRANSLATION_LANGUAGE_CURRENT', 'xx-et-current');
*/
define('ENTITY_TRANSLATION_LANGUAGE_AUTHOR', 'xx-et-author');
/**
* Defines an i18n translation mode for Entity Translation.
*/
define('I18N_MODE_ENTITY_TRANSLATION', 32768);
/**
* Implements hook_hook_info().
@@ -105,8 +111,11 @@ function entity_translation_entity_info() {
'translation' => array(
'entity_translation' => array(
'class' => 'EntityTranslationTaxonomyTermHandler',
'access callback' => 'entity_translation_taxonomy_term_tab_access',
'access arguments' => array(1),
'base path' => 'taxonomy/term/%taxonomy_term',
'edit form' => 'term',
'bundle callback' => 'entity_translation_taxonomy_term_enabled_vocabulary',
),
),
);
@@ -129,7 +138,7 @@ function entity_translation_entity_info() {
* Processes the given path schemes and fill-in default values.
*/
function _entity_translation_process_path_schemes($entity_type, &$et_info) {
$path_scheme_keys = array_flip(array('base path', 'view path', 'edit path', 'translate path', 'path wildcard', 'admin theme'));
$path_scheme_keys = array_flip(array('base path', 'view path', 'edit path', 'translate path', 'path wildcard', 'admin theme', 'edit tabs'));
// Insert the default path scheme into the 'path schemes' array and remove
// respective elements from the entity_translation info array.
@@ -164,6 +173,7 @@ function _entity_translation_process_path_schemes($entity_type, &$et_info) {
$et_info['path schemes'][$delta] += array(
'admin theme' => TRUE,
'path wildcard' => "%$entity_type",
'edit tabs' => TRUE,
);
}
}
@@ -184,6 +194,9 @@ function entity_translation_entity_info_alter(&$entity_info) {
// not. As a matter of fact we might need them to correctly switch field
// translatability when a field is shared across different entity types.
$et_info += array('class' => 'EntityTranslationDefaultHandler');
if (!isset($entity_info[$entity_type]['entity keys'])) {
$entity_info[$entity_type]['entity keys'] = array();
}
$entity_info[$entity_type]['entity keys'] += array('translations' => 'translations');
if (entity_translation_enabled($entity_type, NULL, TRUE)) {
@@ -231,6 +244,13 @@ function entity_translation_menu() {
'file' => 'entity_translation.admin.inc',
);
$items['entity_translation/taxonomy_term/autocomplete'] = array(
'title' => 'Entity translation autocomplete',
'page callback' => 'entity_translation_taxonomy_term_autocomplete',
'access arguments' => array('access content'),
'type' => MENU_CALLBACK,
);
return $items;
}
@@ -286,7 +306,7 @@ function _entity_translation_validate_path_schemes(&$schemes, $entity_type_label
if (empty($scheme['path wildcard'])) {
if ($warnings) {
$t_args = array('%scheme' => $delta, '%entity_type' => $entity_type_label);
drupal_set_message(t('Entity Translation path scheme %scheme for entities of type %entity_type does not declare a path wildcard.', $t_args), 'warning');
watchdog('entity_translation', 'Entity Translation path scheme %scheme for entities of type %entity_type does not declare a path wildcard.', $t_args);
}
unset($schemes[$delta]);
continue;
@@ -444,7 +464,11 @@ function entity_translation_menu_alter(&$items) {
// Replace the main edit callback with our proxy implementation to set
// form language to the current language and check access.
$entity_position = array_search($scheme['path wildcard'], $edit_path_parts);
$original_item = $edit_item;
// Make sure incoming page and access arguments are arrays.
$original_item = $edit_item + array(
'page arguments' => array(),
'access arguments' => array(),
);
$args = array($entity_type, $entity_position, FALSE, $original_item);
$edit_item['page callback'] = 'entity_translation_edit_page';
$edit_item['page arguments'] = array_merge($args, $original_item['page arguments']);
@@ -452,21 +476,23 @@ function entity_translation_menu_alter(&$items) {
$edit_item['access arguments'] = array_merge($args, $original_item['access arguments']);
// Edit translation callback.
$translation_position = count($edit_path_parts);
$args = array($entity_type, $entity_position, $translation_position, $original_item);
$items["$edit_path/%entity_translation_language"] = array(
'type' => MENU_DEFAULT_LOCAL_TASK,
'title callback' => 'entity_translation_edit_title',
'title arguments' => array($translation_position),
'page callback' => 'entity_translation_edit_page',
'page arguments' => array_merge($args, $original_item['page arguments']),
'access callback' => 'entity_translation_edit_access',
'access arguments' => array_merge($args, $original_item['access arguments']),
)
// We need to inherit the remaining menu item keys, mostly 'module'
// and 'file' to keep ajax callbacks working (see form_get_cache() and
// drupal_retrieve_form()).
+ $original_item;
if ($scheme['edit tabs'] !== FALSE) {
$translation_position = count($edit_path_parts);
$args = array($entity_type, $entity_position, $translation_position, $original_item);
$items["$edit_path/%entity_translation_language"] = array(
'type' => MENU_DEFAULT_LOCAL_TASK,
'title callback' => 'entity_translation_edit_title',
'title arguments' => array($translation_position),
'page callback' => 'entity_translation_edit_page',
'page arguments' => array_merge($args, $original_item['page arguments']),
'access callback' => 'entity_translation_edit_access',
'access arguments' => array_merge($args, $original_item['access arguments']),
)
// We need to inherit the remaining menu item keys, mostly 'module'
// and 'file' to keep ajax callbacks working (see form_get_cache() and
// drupal_retrieve_form()).
+ $original_item;
}
// Add translation callback.
$add_path = "$edit_path/add/%entity_translation_language/%entity_translation_language";
@@ -498,12 +524,13 @@ function entity_translation_menu_alter(&$items) {
}
if ($translate_ui_attached == FALSE) {
drupal_set_message(t('The entities of type %entity_type do not define a valid path scheme: it will not be possible to translate them.', array('%entity_type' => $info['label'])), 'warning');
watchdog('entity_translation', 'The entities of type %entity_type do not define a valid path scheme: it will not be possible to translate them.', array('%entity_type' => $info['label']));
}
// Node-specific menu alterations.
if ($entity_type == 'node') {
entity_translation_node_menu_alter($items, $backup);
// Entity-type-specific menu alterations.
$function = 'entity_translation_' . $entity_type . '_menu_alter';
if (function_exists($function)) {
$function($items, $backup);
}
}
}
@@ -533,8 +560,8 @@ function entity_translation_edit_page() {
// Set the current form language.
$handler = entity_translation_get_handler($entity_type, $entity);
$handler->initPathScheme();
$langcode = entity_translation_form_language($langcode, $handler);
$handler->setFormLanguage($langcode);
$langcode = entity_translation_get_existing_language($entity_type, $entity, $langcode);
$handler->setActiveLanguage($langcode);
// Display the entity edit form.
return _entity_translation_callback($edit_form_item['page callback'], $args, $edit_form_item);
@@ -548,10 +575,20 @@ function entity_translation_edit_access() {
$entity_type = array_shift($args);
$entity = array_shift($args);
$langcode = array_shift($args);
$edit_form_item = array_shift($args);
$access_callback = isset($edit_form_item['access callback']) ? $edit_form_item['access callback'] : 'user_access';
$handler = entity_translation_get_handler($entity_type, $entity);
// First, check a handler has been loaded. This could be empty if a
// non-existent entity edit path has been requested, for example. Delegate
// directly to the edit form item access callback in this case.
if (empty($handler)) {
return _entity_translation_callback($access_callback, $args, $edit_form_item);
}
$translations = $handler->getTranslations();
$langcode = entity_translation_form_language($langcode, $handler);
$langcode = entity_translation_get_existing_language($entity_type, $entity, $langcode);
// The user must be explicitly allowed to access the original values if
// workflow permissions are enabled.
@@ -567,8 +604,7 @@ function entity_translation_edit_access() {
// is language neutral we need to let editors access it.
$enabled_languages = entity_translation_languages($entity_type, $entity);
if (isset($enabled_languages[$langcode]) || $langcode == LANGUAGE_NONE) {
$edit_form_item = array_shift($args);
return _entity_translation_callback($edit_form_item['access callback'], $args, $edit_form_item);
return _entity_translation_callback($access_callback, $args, $edit_form_item);
}
}
@@ -578,11 +614,6 @@ function entity_translation_edit_access() {
/**
* Determines the current form language.
*
* Based on the requested language and the translations available for the entity
* being edited, determines the active form language. This takes into account
* language fallback rules so that the translation being edited matches the one
* being viewed.
*
* @param $langcode
* The requested language code.
* @param EntityTranslationHandlerInterface $handler
@@ -590,8 +621,35 @@ function entity_translation_edit_access() {
*
* @return
* A valid language code.
*
* @deprecated This is no longer used and will be removed in the first stable
* release.
*/
function entity_translation_form_language($langcode, $handler) {
return entity_translation_get_existing_language($handler->getEntity(), $handler->getEntityType(), $langcode);
}
/**
* Determines an existing translation language.
*
* Based on the requested language and the translations available for the given
* entity, determines an existing translation language. This takes into account
* language fallback rules.
*
* @param $entity_type
* The type of the entity.
* @param $entity
* The entity whose existing translation language has to be returned.
* @param $langcode
* (optional) The requested language code. Defaults to the current content
* language.
*
* @return
* A valid language code.
*/
function entity_translation_get_existing_language($entity_type, $entity, $langcode = NULL) {
$handler = entity_translation_get_handler($entity_type, $entity);
if (empty($langcode)) {
$langcode = $GLOBALS['language_content']->language;
}
@@ -625,7 +683,8 @@ function entity_translation_add_access() {
$enabled_languages = entity_translation_languages($entity_type, $entity);
if (isset($enabled_languages[$langcode])) {
$edit_form_item = array_shift($args);
return _entity_translation_callback($edit_form_item['access callback'], $args, $edit_form_item);
$access_callback = isset($edit_form_item['access callback']) ? $edit_form_item['access callback'] : 'user_access';
return _entity_translation_callback($access_callback, $args, $edit_form_item);
}
}
@@ -645,7 +704,7 @@ function entity_translation_add_page() {
$handler = entity_translation_get_handler($entity_type, $entity);
$handler->initPathScheme();
$handler->setFormLanguage($langcode);
$handler->setActiveLanguage($langcode);
$handler->setSourceLanguage($source);
// Display the entity edit form.
@@ -669,7 +728,7 @@ function _entity_translation_callback($callback, $args, $info = array()) {
function entity_translation_admin_paths() {
$paths = array();
foreach (entity_get_info() as $entity_type => $info) {
if (entity_translation_enabled($entity_type, NULL, TRUE)) {
if (isset($info['translation']['entity_translation']['path schemes']) && entity_translation_enabled($entity_type, NULL, TRUE)) {
foreach ($info['translation']['entity_translation']['path schemes'] as $scheme) {
if (!empty($scheme['admin theme'])) {
if (isset($scheme['translate path'])) {
@@ -693,8 +752,12 @@ function entity_translation_admin_paths() {
*/
function entity_translation_tab_access($entity_type, $entity) {
if (drupal_multilingual() && (user_access('translate any entity') || user_access("translate $entity_type entities"))) {
$handler = entity_translation_get_handler($entity_type, $entity);
// Ensure $entity holds an entity object and not an id.
$entity = $handler->getEntity();
$enabled = entity_translation_enabled($entity_type, $entity);
return $enabled && entity_translation_get_handler($entity_type, $entity)->getLanguage() != LANGUAGE_NONE;
return $enabled && $handler->getLanguage() != LANGUAGE_NONE;
}
return FALSE;
}
@@ -866,7 +929,7 @@ function entity_translation_field_language_alter(&$display_language, $context) {
if (isset($translations->data[$context['language']]) && !entity_translation_access($entity_type, $translations->data[$context['language']])) {
list(, , $bundle) = entity_extract_ids($entity_type, $entity);
$instances = field_info_instances($entity_type, $bundle);
$entity = clone($entity);
$entity = clone $entity;
foreach ($translations->data as $langcode => $translation) {
if ($langcode == $context['language'] || !entity_translation_access($entity_type, $translations->data[$langcode])) {
@@ -1058,6 +1121,10 @@ function entity_translation_sync($entity_type, $entity) {
// If an item has been removed we do not store its translations.
if ($removed) {
// Ensure items are actually removed.
if (!isset($entity->{$field_name}[$langcode])) {
$entity->{$field_name}[$langcode] = array();
}
continue;
}
// If a synchronized column has changed we need to override the full
@@ -1118,14 +1185,27 @@ function entity_translation_field_attach_delete($entity_type, $entity) {
}
}
/**
* Implements hook_field_attach_delete_revision().
*/
function entity_translation_field_attach_delete_revision($entity_type, $entity) {
if (entity_translation_enabled($entity_type, $entity)) {
$handler = entity_translation_get_handler($entity_type, $entity);
$handler->removeRevisionTranslations();
$handler->saveTranslations();
}
}
/**
* Implementation of hook_field_attach_form().
*/
function entity_translation_field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode) {
// Skip recursing into the source form.
if (empty($form['#entity_translation_source_form']) && ($handler = entity_translation_entity_form_get_handler($form, $form_state))) {
// Avoid recursing into the source form.
list($id, , $bundle) = entity_extract_ids($entity_type, $entity);
if (empty($form['#entity_translation_source_form']) && entity_translation_enabled($entity_type, $bundle)) {
$handler = entity_translation_get_handler($entity_type, $entity);
$langcode = !empty($langcode) ? $langcode : $handler->getLanguage();
$form_langcode = $handler->getFormLanguage();
$form_langcode = $handler->getActiveLanguage();
$translations = $handler->getTranslations();
$update_langcode = $form_langcode && ($form_langcode != $langcode);
$source = $handler->getSourceLanguage();
@@ -1140,8 +1220,6 @@ function entity_translation_field_attach_form($entity_type, $entity, &$form, &$f
// with the correct form language and replace the field elements with the
// correct ones.
if ($update_langcode || ($source && !isset($translations->data[$form_langcode]) && isset($translations->data[$source]) && empty($form_state['rebuild']))) {
list(, , $bundle) = entity_extract_ids($entity_type, $entity);
foreach (field_info_instances($entity_type, $bundle) as $instance) {
$field_name = $instance['field_name'];
$field = field_info_field($field_name);
@@ -1150,26 +1228,31 @@ function entity_translation_field_attach_form($entity_type, $entity, &$form, &$f
// language information from source to target language, this way the
// user can find the form items already populated with the source values
// while the field form element holds the correct language information.
if ($field['translatable']) {
$form[$field_name]['#field_name'] = $field_name;
$form[$field_name]['#source'] = $update_langcode ? $form_langcode : $source;
$form[$field_name]['#previous'] = NULL;
if ($field['translatable'] && isset($form[$field_name])) {
$element = &$form[$field_name];
$element['#entity_type'] = $entity_type;
$element['#entity'] = $entity;
$element['#entity_id'] = $id;
$element['#field_name'] = $field_name;
$element['#source'] = $update_langcode ? $form_langcode : $source;
$element['#previous'] = NULL;
$element['#form_parents'] = $form['#parents'];
// If we are updating the form language we need to make sure that the
// wrong language is unset and the right one is stored in the field
// element (see entity_translation_prepare_element()).
if ($update_langcode) {
$form[$field_name]['#previous'] = $form[$field_name]['#language'];
$form[$field_name]['#language'] = $form_langcode;
$element['#previous'] = $element['#language'];
$element['#language'] = $form_langcode;
}
// Swap default values during form processing to avoid recursion. We
// try to act before any other callback so that the correct values are
// already in place for them.
if (!isset($form[$field_name]['#process'])) {
$form[$field_name]['#process'] = array();
if (!isset($element['#process'])) {
$element['#process'] = array();
}
array_unshift($form[$field_name]['#process'], 'entity_translation_prepare_element');
array_unshift($element['#process'], 'entity_translation_prepare_element');
}
}
}
@@ -1181,20 +1264,23 @@ function entity_translation_field_attach_form($entity_type, $entity, &$form, &$f
list(, , $bundle) = entity_extract_ids($entity_type, $entity);
foreach (field_info_instances($entity_type, $bundle) as $instance) {
$field_name = $instance['field_name'];
$field = field_info_field($field_name);
// If access is not set or is granted we check whether the user has
// access to shared fields.
$form[$field_name]['#access'] = (!isset($form[$field_name]['#access']) || $form[$field_name]['#access']) && ($field['translatable'] || $shared_access);
$form[$field_name]['#multilingual'] = (boolean) $field['translatable'];
// Check if a field is part of the form array.
if (isset($form[$field_name])) {
$field = field_info_field($field_name);
// If access is not set or is granted we check whether the user has
// access to shared fields.
$form[$field_name]['#access'] = (!isset($form[$field_name]['#access']) || $form[$field_name]['#access']) && ($field['translatable'] || $shared_access);
$form[$field_name]['#multilingual'] = (boolean) $field['translatable'];
}
}
}
// If a translation is being created and no path alias exists for its
// language, by default an alias needs to be generated. The standard
// behavior is defaulting to FALSE when an entity already exists, hence we
// need to override it here.
// If a translation is being created, an alias may be generated if needed.
// The standard behavior is defaulting to FALSE when an entity already
// exists, hence we need to override it here.
// If needed, a variable is provided for reverting to the default behavior.
if (module_exists('pathauto') && $handler->getSourceLanguage()) {
$entity->path['pathauto'] = TRUE;
$entity->path['pathauto'] = variable_get('entity_translation_pathauto_state_active_new_translation', TRUE);
}
}
}
@@ -1203,18 +1289,37 @@ function entity_translation_field_attach_form($entity_type, $entity, &$form, &$f
* Form element process callback.
*/
function entity_translation_prepare_element($element, &$form_state) {
$source_form = &drupal_static(__FUNCTION__, array());
static $drupal_static_fast;
if (!isset($drupal_static_fast)) {
$drupal_static_fast = &drupal_static(__FUNCTION__, array(
'source_forms' => array(),
'source_form_states' => array(),
));
}
$source_forms = &$drupal_static_fast['source_forms'];
$source_form_states = &$drupal_static_fast['source_form_states'];
$form = $form_state['complete form'];
$build_id = $form['#build_id'];
$source = $element['#source'];
$entity_type = $element['#entity_type'];
$id = $element['#entity_id'];
if (!isset($source_form[$build_id][$source])) {
$source_form[$build_id][$source] = array('#entity_translation_source_form' => TRUE);
// Key the source form cache per entity type and entity id to allow for
// multiple entities on the same entity form.
if (!isset($source_forms[$build_id][$source][$entity_type][$id])) {
$source_form = array(
'#entity_translation_source_form' => TRUE,
'#parents' => $element['#form_parents'],
);
$source_form_state = $form_state;
$info = entity_translation_edit_form_info($form, $form_state);
field_attach_form($info['entity type'], $info['entity'], $source_form[$build_id][$source], $source_form_state, $source);
field_attach_form($entity_type, $element['#entity'], $source_form, $source_form_state, $source);
$source_forms[$build_id][$source][$entity_type][$id] = &$source_form;
$source_form_states[$build_id][$source][$entity_type][$id] = &$source_form_state;
}
$source_form = &$source_forms[$build_id][$source][$entity_type][$id];
$source_form_state = $source_form_states[$build_id][$source][$entity_type][$id];
$langcode = $element['#language'];
$field_name = $element['#field_name'];
@@ -1222,15 +1327,45 @@ function entity_translation_prepare_element($element, &$form_state) {
// language information from source to target language, this way the user can
// find the form items already populated with the source values while the
// field form element holds the correct language information.
if (isset($source_form[$build_id][$source][$field_name][$source])) {
$element[$langcode] = $source_form[$build_id][$source][$field_name][$source];
if (isset($source_form[$field_name][$source])) {
$element[$langcode] = $source_form[$field_name][$source];
entity_translation_form_element_language_replace($element, $source, $langcode);
entity_translation_form_element_state_replace($element, $source_form[$field_name], $field_name, $source_form_state, $form_state);
unset($element[$element['#previous']]);
}
return $element;
}
/**
* Helper function. Sets the right values in $form_state['field'] when using
* source language values as defaults.
*/
function entity_translation_form_element_state_replace($element, $source_element, $field_name, $source_form_state, &$form_state) {
if (isset($source_element['#language'])) {
$source = $source_element['#language'];
// Iterate through the form structure recursively.
foreach (element_children($element) as $key) {
if (isset($source_element[$key])) {
entity_translation_form_element_state_replace($element[$key], $source_element[$key], $key, $source_form_state, $form_state);
}
elseif (isset($source_element[$source])) {
entity_translation_form_element_state_replace($element[$key], $source_element[$source], $key, $source_form_state, $form_state);
}
}
if (isset($source_element[$source]['#field_parents'])) {
$source_parents = $source_element[$source]['#field_parents'];
$langcode = $element['#language'];
$parents = $element[$langcode]['#field_parents'];
$source_state = field_form_get_state($source_parents, $field_name, $source, $source_form_state);
drupal_alter('entity_translation_source_field_state', $source_state);
field_form_set_state($parents, $field_name, $langcode, $form_state, $source_state);
}
}
}
/**
* Helper function. Recursively replaces the source language with the given one.
*/
@@ -1243,7 +1378,7 @@ function entity_translation_form_element_language_replace(&$element, $source, $l
// Replace specific occurrences of the source language with the target
// language.
foreach (element_properties($element) as $key) {
if ($key === '#language') {
if ($key === '#language' && $element[$key] != LANGUAGE_NONE) {
$element[$key] = $langcode;
}
elseif ($key === '#parents' || $key === '#field_parents') {
@@ -1343,6 +1478,10 @@ function _entity_translation_element_title_append(&$element, $suffix) {
if (isset($element['#type']) && isset($fapi_title_elements[$element['#type']]) && isset($element['#title'])) {
$element['#title'] .= $suffix;
}
// If this is a multi-valued field, apply the suffix to the container.
elseif (isset($element['#title']) && isset($element['#cardinality']) && $element['#cardinality'] != 1) {
$element['#title'] .= $suffix;
}
// If the current element does not have a (valid) title, try child elements.
elseif ($children = element_children($element)) {
foreach ($children as $delta) {
@@ -1360,30 +1499,32 @@ function _entity_translation_element_title_append(&$element, $suffix) {
* Implements hook_form_alter().
*/
function entity_translation_form_alter(&$form, &$form_state) {
if ($handler = entity_translation_entity_form_get_handler($form, $form_state)) {
if (!$handler->isNewEntity()) {
$handler->entityForm($form, $form_state);
$translations = $handler->getTranslations();
$form_langcode = $handler->getFormLanguage();
if (!isset($translations->data[$form_langcode]) || count($translations->data) > 1) {
// Hide shared form elements if the user is not allowed to edit them.
$handler->entityFormSharedElements($form);
if ($info = entity_translation_edit_form_info($form, $form_state)) {
$handler = entity_translation_get_handler($info['entity type'], $info['entity']);
if (entity_translation_enabled($info['entity type'], $info['entity'])) {
if (!$handler->isNewEntity()) {
$handler->entityForm($form, $form_state);
$translations = $handler->getTranslations();
$form_langcode = $handler->getActiveLanguage();
if (!isset($translations->data[$form_langcode]) || count($translations->data) > 1) {
// Hide shared form elements if the user is not allowed to edit them.
$handler->entityFormSharedElements($form);
}
}
else {
$handler->entityFormLanguageWidget($form, $form_state);
}
// We need to process the posted form as early as possible to update the
// form language value.
array_unshift($form['#validate'], 'entity_translation_entity_form_validate');
}
// We might have an entity form for an entity or a bundle not enabled for
// translation. In this case we might need to deal with entity and field
// languages anyway, since fields may be shared among different bundles and
// entity types.
else {
$handler->entityFormLanguageWidget($form, $form_state);
}
// We need to process the posted form as early as possible to update the
// form language value.
array_unshift($form['#validate'], 'entity_translation_entity_form_validate');
}
// We might have an entity form for an entity or a bundle not enabled for
// translation. In this case we might need to deal with entity and field
// languages anyway, since fields may be shared among different bundles and
// entity types.
elseif ($info = entity_translation_edit_form_info($form, $form_state)) {
$handler = entity_translation_get_handler($info['entity type'], $info['entity']);
$handler->entityFormLanguageWidget($form, $form_state);
}
}
@@ -1393,7 +1534,7 @@ function entity_translation_form_alter(&$form, &$form_state) {
function entity_translation_entity_form_source_language_submit($form, &$form_state) {
$handler = entity_translation_entity_form_get_handler($form, $form_state);
$langcode = $form_state['values']['source_language']['language'];
$path = "{$handler->getEditPath()}/add/$langcode/{$handler->getFormLanguage()}";
$path = "{$handler->getEditPath()}/add/$langcode/{$handler->getActiveLanguage()}";
$options = array();
if (isset($_GET['destination'])) {
$options['query'] = drupal_get_destination();
@@ -1409,7 +1550,7 @@ function entity_translation_entity_form_source_language_submit($form, &$form_sta
*/
function entity_translation_entity_form_delete_translation_submit($form, &$form_state) {
$handler = entity_translation_entity_form_get_handler($form, $form_state);
$path = "{$handler->getTranslatePath()}/delete/{$handler->getFormLanguage()}";
$path = "{$handler->getTranslatePath()}/delete/{$handler->getActiveLanguage()}";
$options = array();
if (isset($_GET['destination'])) {
$options['query'] = drupal_get_destination();
@@ -1429,16 +1570,22 @@ function entity_translation_entity_form_validate($form, &$form_state) {
}
/**
* Submit handler for the entity language widget.
* Validation handler for the entity language widget.
*/
function entity_translation_language_widget_submit($form, &$form_state) {
function entity_translation_entity_form_language_update($element, &$form_state, $form) {
$handler = entity_translation_entity_form_get_handler($form, $form_state);
// On non-translatable entities, we need to handle just the entity and field
// language.
if (empty($handler) && ($info = entity_translation_edit_form_info($form, $form_state))) {
$handler = entity_translation_get_handler($info['entity type'], $info['entity']);
// Ensure the handler form language match the actual one. This is mainly
// needed when responding to an AJAX request where the languages cannot be set
// from the usual page callback.
if (!empty($form_state['entity_translation']['form_langcode'])) {
$handler->setActiveLanguage($form_state['entity_translation']['form_langcode']);
}
// When responding to an AJAX request we should ignore any change in the
// language widget as it may alter the field language expected by the AJAX
// callback.
if (empty($form_state['triggering_element']['#ajax'])) {
$handler->entityFormLanguageWidgetSubmit($form, $form_state);
}
$handler->entityFormLanguageWidgetSubmit($form, $form_state);
}
/**
@@ -1460,10 +1607,13 @@ function entity_translation_entity_form_submit($form, &$form_state) {
* Mark translations as outdated if the submitted value is true.
*/
function entity_translation_field_attach_submit($entity_type, $entity, $form, &$form_state) {
if ($handler = entity_translation_entity_form_get_handler($form, $form_state)) {
if (($handler = entity_translation_entity_form_get_handler($form, $form_state)) && entity_translation_enabled($entity_type, $entity)) {
// Update the wrapped entity with the submitted values.
$handler->setEntity($entity);
$handler->entityFormSubmit($form, $form_state);
// Process in-place translations for the taxonomy autocomplete widget.
entity_translation_taxonomy_term_field_attach_submit($entity_type, $entity, $form, $form_state);
}
}
@@ -1538,7 +1688,7 @@ function theme_entity_translation_language_tabs($variables) {
* Adds an option to enable field synchronization.
* Enable a selector to choose whether a field is translatable.
*/
function entity_translation_form_field_ui_field_edit_form_alter(&$form, $form_state) {
function entity_translation_form_field_ui_field_edit_form_alter(&$form, &$form_state) {
$instance = $form['#instance'];
$entity_type = $instance['entity_type'];
$field_name = $instance['field_name'];
@@ -1558,7 +1708,8 @@ function entity_translation_form_field_ui_field_edit_form_alter(&$form, $form_st
$label = t('Field translation');
$title = t('Users may translate all occurrences of this field:') . _entity_translation_field_desc($field);
if (field_has_data($field)) {
$form_state['field_has_data'] = field_has_data($field);
if ($form_state['field_has_data']) {
$path = "admin/config/regional/entity_translation/translatable/$field_name";
$status = $translatable ? $title : (t('All occurrences of this field are untranslatable:') . _entity_translation_field_desc($field));
$link_title = !$translatable ? t('Enable translation') : t('Disable translation');
@@ -1586,6 +1737,11 @@ function entity_translation_form_field_ui_field_edit_form_alter(&$form, $form_st
'#default_value' => $translatable,
);
}
$function = 'entity_translation_form_field_ui_field_edit_' . $instance['widget']['type'] . '_form_alter';
if (function_exists($function)) {
$function($form, $form_state);
}
}
/**
@@ -1646,6 +1802,9 @@ function entity_translation_enabled($entity_type, $entity = NULL, $skip_handler
/**
* Determines whether the given entity bundle is translatable.
*
* NOTE: Does not check for whether the entity type is translatable.
* Consider using entity_translation_enabled() instead.
*
* @param $entity_type
* The entity type the bundle to be checked belongs to.
* @param $bundle
@@ -1685,54 +1844,37 @@ function entity_translation_settings($entity_type, $bundle) {
* Entity language callback.
*
* This callback changes the entity language from the actual one to the active
* form language. This overriding allows to obtain language dependent form
* widgets where multilingual values are supported (e.g. field or path alias
* widgets) even if the code was not originally written with supporting multiple
* values per language in mind.
* language. This overriding allows to obtain language dependent form widgets
* where multilingual values are supported (e.g. field or path alias widgets)
* even if the code was not originally written with supporting multiple values
* per language in mind.
*
* The main drawback of this approach is that code needing to access the actual
* language in the entity form build/validation/submit workflow cannot rely on
* the entity_language() function. On the other hand in these scenarios assuming
* the presence of Entity translation should be safe, thus being able to rely on
* the EntityTranslationHandlerInterface::getLanguage() method.
* the presence of Entity translation should be safe, thus developers can rely
* on the EntityTranslationHandlerInterface::getLanguage() method.
*
* @param $entity_type
* @param string $entity_type
* The the type of the entity.
* @param $entity
* @param object $entity
* The entity whose language has to be returned.
*
* @return
* @return string
* A valid language code.
*/
function entity_translation_language($entity_type, $entity) {
// If a form has been post, we need to check its state to verify if any form
// translation handler is stored there. This is mainly needed when responding
// to an AJAX request where the form language cannot be set from the page
// callback.
$handler = entity_translation_current_form_get_handler();
// Make sure we always have a translation handler instance available.
if (empty($handler)) {
$handler = entity_translation_get_handler($entity_type, $entity);
$handler = entity_translation_get_handler($entity_type, $entity);
if (!$handler) {
return LANGUAGE_NONE;
}
// If a translation handler associated to the current form is found, we need
// to update the wrapped entity. This way submitted values will be picked up.
// Other entities may be created or saved while submitting the current one,
// hence we need to check we are dealing with it.
elseif ($handler->isWrappedEntity($entity_type, $entity)) {
$langcode = $handler->getLanguage();
$handler->setEntity($entity);
$submitted_langcode = $handler->getLanguage();
// If the entity language has changed we are editing the original values. In
// this case we need to update the current form language with the submitted
// one.
if ($submitted_langcode != $langcode) {
$handler->setFormLanguage($submitted_langcode);
}
if (entity_translation_enabled($entity_type, $entity)) {
$langcode = $handler->getActiveLanguage();
return $langcode ? $langcode : $handler->getLanguage();
}
else {
return $handler->getLanguage();
}
$langcode = $handler->getFormLanguage();
return !empty($langcode) ? $langcode : $handler->getLanguage();
}
/**
@@ -1743,61 +1885,13 @@ function entity_translation_language($entity_type, $entity) {
* @param $entity
* (optional) The entity to be translated. A bundle name may be passed to
* instantiate an empty entity.
* @param $update
* (optional) Instances are statically cached: if this is TRUE the wrapped
* entity will be replaced by the passed one.
*
* @return EntityTranslationHandlerInterface
* A class implementing EntityTranslationHandlerInterface.
*/
function entity_translation_get_handler($entity_type = NULL, $entity = NULL, $update = FALSE) {
static $drupal_static_fast;
if (!isset($drupal_static_fast['handlers'])) {
$drupal_static_fast['handlers'] = &drupal_static(__FUNCTION__, array());
}
$handlers = &$drupal_static_fast['handlers'];
// Workaround the lack of a context object.
if (empty($entity)) {
if (isset($handlers[$entity_type]['#current'])) {
return $handlers[$entity_type]['#current'];
}
elseif (empty($entity_type) && isset($handlers['#current']['#current'])) {
return $handlers['#current']['#current'];
}
else {
return NULL;
}
}
elseif (is_string($entity)) {
$entity = entity_create_stub_entity($entity_type, array(NULL, NULL, $entity));
}
list($entity_id) = entity_extract_ids($entity_type, $entity);
if (!isset($handlers[$entity_type][$entity_id])) {
$entity_info = entity_get_info($entity_type);
$class = $entity_info['translation']['entity_translation']['class'];
// @todo remove fourth parameter once 3rd-party translation handlers have
// been fixed and no longer require the deprecated entity_id parameter.
$handler = new $class($entity_type, $entity_info, $entity, NULL);
// If the entity id is empty we cannot cache the translation handler
// instance.
if (empty($entity_id)) {
return $handler;
}
else {
$handlers[$entity_type][$entity_id] = $handler;
}
}
elseif ($update) {
$handlers[$entity_type][$entity_id]->setEntity($entity);
}
$handlers[$entity_type]['#current'] = $handlers[$entity_type][$entity_id];
$handlers['#current']['#current'] = $handlers[$entity_type][$entity_id];
return $handlers[$entity_type][$entity_id];
function entity_translation_get_handler($entity_type = NULL, $entity = NULL) {
$factory = EntityTranslationHandlerFactory::getInstance();
return empty($entity) ? $factory->getLastHandler($entity_type) : $factory->getHandler($entity_type, $entity);
}
/**
@@ -1811,24 +1905,11 @@ function entity_translation_get_handler($entity_type = NULL, $entity = NULL, $up
* @return EntityTranslationHandlerInterface
* A class implementing EntityTranslationHandlerInterface.
*/
function entity_translation_entity_form_get_handler($form, &$form_state) {
function entity_translation_entity_form_get_handler($form, $form_state) {
$handler = FALSE;
$entity_type = isset($form['#entity_type']) && is_string($form['#entity_type']) ? $form['#entity_type'] : FALSE;
if ($entity_type) {
if (empty($form_state['storage']['entity_translation']['handler'][$entity_type])) {
if ($info = entity_translation_edit_form_info($form, $form_state)) {
if (entity_translation_enabled($info['entity type'], $info['entity'])) {
$handler = entity_translation_get_handler($info['entity type'], $info['entity']);
$form_state['storage']['entity_translation']['handler'][$info['entity type']] = $handler;
}
}
}
else {
$handler = $form_state['storage']['entity_translation']['handler'][$entity_type];
}
if ($info = entity_translation_edit_form_info($form, $form_state)) {
$handler = entity_translation_get_handler($info['entity type'], $info['entity']);
}
return $handler;
}
@@ -1837,21 +1918,25 @@ function entity_translation_entity_form_get_handler($form, &$form_state) {
*
* @return EntityTranslationHandlerInterface
* A translation handler instance if available, FALSE oterwise.
*
* @deprecated This is no longer used and will be removed in the first stable
* release.
*/
function entity_translation_current_form_get_handler() {
$handler = FALSE;
if (!empty($_POST['form_build_id'])) {
$form_state = form_state_defaults();
$form = form_get_cache($_POST['form_build_id'], $form_state);
$handler = entity_translation_entity_form_get_handler($form, $form_state);
if ($form = form_get_cache($_POST['form_build_id'], $form_state)) {
$handler = entity_translation_entity_form_get_handler($form, $form_state);
}
}
return $handler;
}
/**
* Returns an array of edit form info as defined in hook_translation_info().
* Returns an array of edit form info as defined in hook_entity_info().
*
* @param $form
* The entity edit form.
@@ -1869,11 +1954,13 @@ function entity_translation_edit_form_info($form, $form_state) {
if ($entity_type) {
$entity_info = entity_get_info($form['#entity_type']);
if (!empty($entity_info['translation']['entity_translation']['edit form'])) {
$entity_key = $entity_info['translation']['entity_translation']['edit form'];
if (isset($form_state[$entity_key])) {
$entity_keys = explode('][', $entity_info['translation']['entity_translation']['edit form']);
$key_exists = FALSE;
$entity = drupal_array_get_nested_value($form_state, $entity_keys, $key_exists);
if ($key_exists) {
$info = array(
'entity type' => $form['#entity_type'],
'entity' => (object) $form_state[$entity_key],
'entity' => (object) $entity,
);
}
}
@@ -1961,31 +2048,50 @@ function entity_translation_entity_uuid_presave(&$entity, $entity_type) {
}
/**
* Implement hook_pathauto_alias_alter().
* Implements hook_pathauto_alias_alter().
*
* When bulk-updating aliases for nodes automatically create a path for every
* translation.
* When updating or bulk-updating aliases for nodes, automatically create a path
* for every translation, dependent on related configuration.
* @see entity_translation_admin_form()
*/
function entity_translation_pathauto_alias_alter(&$alias, array &$context) {
$info = entity_get_info();
$entity_type = $context['module'];
$operations_modes = array(
'bulkupdate' => variable_get('entity_translation_pathauto_state_mode_bulkupdate', 'generate_all_aliases_all_languages'),
'update' => variable_get('entity_translation_pathauto_state_mode_update', 'generate_alias_active_language'),
);
$operation = $context['op'];
// Ensure that we are dealing with a bundle having entity translation enabled.
if ($context['op'] == 'bulkupdate' && !empty($info[$entity_type]['token type']) && !empty($context['data'][$info[$entity_type]['token type']])) {
$entity = $context['data'][$info[$entity_type]['token type']];
if (entity_translation_enabled($entity_type, $entity)) {
$translations = entity_translation_get_handler($entity_type, $entity)->getTranslations();
// Only create extra aliases if we are working on the original language to
// avoid infinite recursion.
if ($context['language'] == $translations->original) {
foreach ($translations->data as $language => $translation) {
// We already have an alias for the original language, so let's not
// create another one.
if ($language == $translations->original) {
continue;
}
pathauto_create_alias($entity_type, $context['op'], $context['source'], $context['data'], $context['type'], $language);
}
// Nothing to do when we don't have a replacement pattern or incoming data.
if (!in_array($operation, array_keys($operations_modes)) || empty($info[$entity_type]['token type']) || empty($context['data'][$info[$entity_type]['token type']])) {
return;
}
// Do not run additional 'update' or 'bulkupdate' operations if the update
// mode is set to generate an alias only for the active language.
if (in_array($operation, array_keys($operations_modes)) &&
$operations_modes[$operation] === 'generate_alias_active_language') {
return;
}
$entity = $context['data'][$info[$entity_type]['token type']];
// Ensure that we are dealing with an entity bundle having entity translation
// enabled.
if (entity_translation_enabled($entity_type, $entity)) {
$translations = entity_translation_get_handler($entity_type, $entity)->getTranslations();
foreach ($translations->data as $language => $translation) {
// We already have an alias for the starting language, so let's not create
// another one.
if ($language === $context['language']) {
continue;
}
$no_existing_alias = (FALSE === _pathauto_existing_alias_data($context['source'], $language));
if ($operations_modes[$operation] === 'generate_all_aliases_all_languages' ||
($no_existing_alias && $operations_modes[$operation] === 'generate_missing_aliases_all_languages')) {
// Modify the context['op'] by appending '_translation', to avoid infinite
// recursion.
pathauto_create_alias($entity_type, $operation . '_translation', $context['source'], $context['data'], $context['type'], $language);
}
}
}
@@ -1999,3 +2105,31 @@ function path_entity_translation_delete($entity_type, $entity, $langcode) {
$handler = entity_translation_get_handler($entity_type, $entity);
path_delete(array('source' => $handler->getViewPath(), 'language' => $langcode));
}
/**
* Wrapper for entity_save().
*
* @param $entity_type
* The entity type.
* @param $entity
* The entity object.
*/
function entity_translation_entity_save($entity_type, $entity) {
// Entity module isn't required, but use it if it's available.
if (module_exists('entity')) {
entity_save($entity_type, $entity);
}
// Fall back to field_attach_* functions otherwise.
else {
field_attach_presave($entity_type, $entity);
field_attach_update($entity_type, $entity);
}
}
/**
* Implements hook_field_attach_prepare_translation_alter().
*/
function entity_translation_field_attach_prepare_translation_alter(&$entity, $context) {
$handler = entity_translation_get_handler('node', $entity);
$handler->setActiveLanguage($context['langcode']);
}
@@ -43,14 +43,15 @@ function entity_translation_node_menu_alter(&$items, $backup) {
$callback['file'] = $item['file'];
$callback['module'] = $item['module'];
$access_arguments = array_merge(array(1, $item['access callback']), $item['access arguments']);
$page_arguments = array_merge(array('node', 1, $callback), $item['page arguments']);
}
else {
$callback = FALSE;
$access_arguments = array(1);
$page_arguments = array('node', 1);
}
$items['node/%node/translate']['page callback'] = 'entity_translation_overview';
$items['node/%node/translate']['page arguments'] = array('node', 1, $callback);
$items['node/%node/translate']['page arguments'] = $page_arguments;
$items['node/%node/translate']['access arguments'] = $access_arguments;
$items['node/%node/translate']['access callback'] = 'entity_translation_node_tab_access';
$items['node/%node/translate']['file'] = 'entity_translation.admin.inc';
@@ -249,3 +250,24 @@ function entity_translation_query_comment_filter_alter(QueryAlterableInterface $
}
}
}
/**
* Implements hook_node_update_index().
*
* Add translated node content to search index.
*/
function entity_translation_node_update_index($node) {
$text = '';
$langcodes = array_keys(language_list());
$translations = entity_translation_get_handler('node', $node)->getTranslations();
foreach ($langcodes as $langcode) {
// Skip the default language (already indexed by search.module).
if ($GLOBALS[LANGUAGE_TYPE_CONTENT]->language != $langcode && isset($translations->data[$langcode])) {
// Render the node in each language.
node_build_content($node, 'search_index', $langcode);
$rendered = drupal_render($node->content);
$text .= '<h1>' . check_plain($node->title) . '</h1>' . $rendered;
}
}
return $text;
}
@@ -0,0 +1,568 @@
<?php
/**
* @file
* The taxonomy specific translation functions and hook implementations.
*/
/**
* Returns whether the given taxonomy vocabulary has support for translations.
*
* @return bool
* TRUE if translation is enabled, FALSE otherwise.
*/
function entity_translation_taxonomy_term_enabled_vocabulary($vocabulary_name) {
$info = variable_get('entity_translation_taxonomy', array());
return !empty($info[$vocabulary_name]);
}
/**
* Taxonomy-term-specific menu alterations.
*/
function entity_translation_taxonomy_term_menu_alter(&$items, $backup) {
if (isset($backup['taxonomy_term'])) {
$item = $backup['taxonomy_term'];
// Preserve the menu router item defined by other modules.
$callback['page callback'] = $item['page callback'];
$callback['file'] = $item['file'];
$callback['module'] = $item['module'];
$access_arguments = array_merge(array(2, $item['access callback']), $item['access arguments']);
$page_arguments = array_merge(array('taxonomy_term', 2, $callback), $item['page arguments']);
}
else {
$access_arguments = array(2);
$page_arguments = array('taxonomy_term', 2);
}
$items['taxonomy/term/%taxonomy_term/translate']['page callback'] = 'entity_translation_overview';
$items['taxonomy/term/%taxonomy_term/translate']['page arguments'] = $page_arguments;
$items['taxonomy/term/%taxonomy_term/translate']['access arguments'] = $access_arguments;
$items['taxonomy/term/%taxonomy_term/translate']['access callback'] = 'entity_translation_taxonomy_term_tab_access';
$items['taxonomy/term/%taxonomy_term/translate']['file'] = 'entity_translation.admin.inc';
$items['taxonomy/term/%taxonomy_term/translate']['module'] = 'entity_translation';
// Delete translation callback.
$items['taxonomy/term/%taxonomy_term/translate/delete/%entity_translation_language']['access arguments'] = $access_arguments;
}
/**
* Taxonomy term specific access callback.
*/
function entity_translation_taxonomy_term_tab_access() {
$args = func_get_args();
$term = array_shift($args);
if (entity_translation_enabled('taxonomy_term', $term)) {
return entity_translation_tab_access('taxonomy_term', $term);
}
else {
$function = array_shift($args);
return $function ? call_user_func_array($function, $args) : FALSE;
}
}
/**
* Implements hook_form_FORM_ID_alter()
*/
function entity_translation_form_taxonomy_form_vocabulary_alter(&$form, &$form_state) {
if (entity_translation_enabled('taxonomy_term')) {
$name = $form_state['vocabulary']->machine_name;
if (isset($form['i18n_translation']['i18n_mode'])) {
$args = array('!url' => url('admin/config/regional/entity_translation'));
$form['i18n_translation']['i18n_mode']['#options'][I18N_MODE_ENTITY_TRANSLATION] = t('Field translation. Term fields will be translated through the <a href="!url">Entity translation</a> module.', $args);
if (entity_translation_enabled_bundle('taxonomy_term', $name)) {
$form['i18n_translation']['i18n_mode']['#default_value'] = I18N_MODE_ENTITY_TRANSLATION;
}
}
else {
$form['entity_translation_taxonomy'] = array(
'#title' => t('Enable field translation'),
'#type' => 'checkbox',
'#prefix' => '<label>' . t('Translation') . '</label>',
'#default_value' => entity_translation_enabled('taxonomy_term', $name),
);
}
$form['#submit'][] = 'entity_translation_form_taxonomy_form_vocabulary_submit';
}
}
/**
* Submit handler for the taxonomy vocabulary form.
*/
function entity_translation_form_taxonomy_form_vocabulary_submit($form, &$form_state) {
if (!empty($form_state['values']['i18n_mode']) && $form_state['values']['i18n_mode'] == I18N_MODE_ENTITY_TRANSLATION) {
$form_state['values']['entity_translation_taxonomy'] = TRUE;
}
$info = variable_get('entity_translation_taxonomy', array());
$info[$form_state['vocabulary']->machine_name] = !empty($form_state['values']['entity_translation_taxonomy']);
variable_set('entity_translation_taxonomy', $info);
}
/**
* Returns a translated label for the specified taxonomy term.
*
* @param object $term
* A taxonomy term object.
* @param string $langcode
* The language the label should be translated in.
*
* @return string
* The translated taxonomy term label.
*
* @internal This is supposed to be used only by the ET taxonomy integration
* code, as it might be removed/replaced in any moment of the ET lifecycle.
*/
function _entity_translation_taxonomy_label($term, $langcode) {
$entity_type = 'taxonomy_term';
if (function_exists('title_entity_label')) {
$label = title_entity_label($term, $entity_type, $langcode);
}
else {
$label = entity_label($entity_type, $term);
}
return (string) $label;
}
/**
* Implements entity_translation_form_field_ui_field_edit_WIDGET_TYPE_form_alter().
*
* {@inheritdoc}
*/
function entity_translation_form_field_ui_field_edit_taxonomy_autocomplete_form_alter(&$form, &$form_state) {
$key = 'entity_translation_taxonomy_autocomplete_translate';
$instance = $form['#instance'];
$field_name = $instance['field_name'];
$entity_type = $instance['entity_type'];
$field = field_info_field($field_name);
$translatable = field_is_translatable($entity_type, $field);
$bundle = !empty($field['settings']['allowed_values'][0]['vocabulary']) ? $field['settings']['allowed_values'][0]['vocabulary'] : NULL;
$access = variable_get('entity_translation_taxonomy_autocomplete', FALSE);
// Add a checkbox to toggle in-place translation for autocomplete widgets.
$form['instance']['settings'][$key] = array(
'#type' => 'checkbox',
'#title' => t('Enable in-place translation of terms'),
'#description' => t('Check this option if you wish to use translation forms to perform in-place translation for terms entered in the original language.'),
'#default_value' => !$translatable && !empty($form['#instance']['settings'][$key]),
'#access' => $access && (!$form_state['field_has_data'] || !$translatable) && entity_translation_enabled('taxonomy_term', $bundle),
'#states' => array(
'visible' => array(':input[name="field[translatable]"]' => array('checked' => FALSE)),
),
'#weight' => -8,
);
}
/**
* Checks whether in-place translation is enabled for the autocomplete widget.
*
* @param array $element
* The widget form element.
* @param array $form_state
* The form state array.
*
* @return bool
* TRUE if in-place translation is enabled, FALSE otherwise.
*/
function _entity_translation_taxonomy_autocomplete_translation_enabled($element, $form_state) {
$field = field_info_field($element['#field_name']);
if (field_is_translatable($element['#entity_type'], $field)) {
return FALSE;
}
list($id, , $bundle) = entity_extract_ids($element['#entity_type'], $element['#entity']);
if (!$id) {
return FALSE;
}
$entity_type = 'taxonomy_term';
$parent_handler = entity_translation_get_handler($element['#entity_type'], $element['#entity']);
$active_langcode = $parent_handler->getActiveLanguage();
$translations = $parent_handler->getTranslations();
$entity_langcode = isset($translations->original) ? $translations->original : LANGUAGE_NONE;
$instance = field_info_instance($element['#entity_type'], $field['field_name'], $bundle);
// We need to make sure that we are not dealing with a translation form.
// However checking the active language is not enough, because the user may
// have changed the entity language.
return
(isset($form_state['entity_translation']['is_translation']) ?
$form_state['entity_translation']['is_translation'] : ($active_langcode != $entity_langcode)) &&
!empty($instance['settings']['entity_translation_taxonomy_autocomplete_translate']) &&
(user_access('translate any entity') || user_access("translate $entity_type entities"));
}
/**
* Implements hook_field_widget_WIDGET_TYPE_form_alter().
*
* {@inheritdoc}
*/
function entity_translation_field_widget_taxonomy_autocomplete_form_alter(&$element, &$form_state, $context) {
// The autocomplete widget is also displayed in the field configuration form,
// in which case we do not need to perform any alteration. To preserve BC, by
// default we enable our taxonomy autocomplete override only on new sites.
if (!isset($element['#entity']) || !variable_get('entity_translation_taxonomy_autocomplete', FALSE)) {
return;
}
// We will need to translate term names, if Title is enabled and configured
// for this vocabulary.
$entity_type = 'taxonomy_term';
$field = field_widget_field($element, $form_state);
$bundle = !empty($field['settings']['allowed_values'][0]['vocabulary']) ? $field['settings']['allowed_values'][0]['vocabulary'] : NULL;
if ($bundle && entity_translation_enabled($entity_type, $bundle)) {
$parent_handler = entity_translation_get_handler($element['#entity_type'], $element['#entity']);
$langcode = $parent_handler->getActiveLanguage();
$terms = array_values(_entity_translation_taxonomy_autocomplete_widget_get_terms($element));
// If we are using the regular autocomplete behavior also in translation
// forms, we need to set our custom callback.
if (!_entity_translation_taxonomy_autocomplete_translation_enabled($element, $form_state)) {
$element['#autocomplete_path'] = 'entity_translation/' . $entity_type . '/autocomplete/' . $langcode . '/' . $element['#field_name'];
$translations = $parent_handler->getTranslations();
if (isset($translations->original) && $translations->original != $langcode) {
$labels = array();
foreach ($terms as $delta => $term) {
$labels[] = _entity_translation_taxonomy_label($term, $langcode);
}
$element['#default_value'] = implode(', ', $labels);
}
}
// Otherwise we just provide the in-place translation widget.
else {
$element['#type'] = 'fieldset';
$element['#description'] = t('Enter one translation for each term');
$element['#access'] = (bool) $terms;
foreach ($terms as $delta => $term) {
$element[$delta] = array(
'#type' => 'textfield',
'#default_value' => _entity_translation_taxonomy_label($term, $langcode),
'#required' => TRUE,
'#tid' => $term->tid,
);
}
$element['#process'][] = 'entity_translation_taxonomy_autocomplete_process';
}
// The native term save logic is performed at widget validation level, so we
// just replace the validation handler to provide our logic instead.
$element['#element_validate'] = array_values(array_diff($element['#element_validate'], array('taxonomy_autocomplete_validate')));
$element['#element_validate'][] = 'entity_translation_taxonomy_autocomplete_validate';
}
}
/**
* Returns the terms referenced by the taxonomy autocomplete widget field.
*
* @param array $element
* The taxonomy autocomplete form element.
*
* @return object[]
* An associative array of taxonomy term object keyed by their identifiers.
*/
function _entity_translation_taxonomy_autocomplete_widget_get_terms($element) {
$items = isset($element['#entity']->{$element['#field_name']}[$element['#language']]) ?
$element['#entity']->{$element['#field_name']}[$element['#language']] : array();
$tids = array_map(function ($item) { return $item['tid']; }, $items);
return taxonomy_term_load_multiple($tids);
}
/**
* Process callback for the ET taxonomy autocomplete widget.
*
* {@inheritdoc}
*/
function entity_translation_taxonomy_autocomplete_process($element) {
// The in-place translation widget makes sense only for untranslatable field,
// which may have the "(all languages)" label suffix. In this case it would be
// confusing so we need to revert that.
$instance = field_info_instance($element['#entity_type'], $element['#field_name'], $element['#bundle']);
$element['#title'] = check_plain($instance['label']);
return $element;
}
/**
* Entity translation taxonomy autocomplete callback.
*
* @param string $langcode
* The input language.
* @param string $field_name
* The name of the term reference field.
* @param string $tags_typed
* (optional) A comma-separated list of term names entered in the
* autocomplete form element. Only the last term is used for autocompletion.
* Defaults to an empty string.
*
* @see taxonomy_autocomplete()
*/
function entity_translation_taxonomy_term_autocomplete($langcode = NULL, $field_name = '', $tags_typed = '') {
// If the request has a '/' in the search text, then the menu system will have
// split it into multiple arguments, recover the intended $tags_typed.
$args = func_get_args();
// Shift off the $langcode and $field_name arguments.
array_shift($args);
array_shift($args);
$tags_typed = implode('/', $args);
// Make sure the field exists and is a taxonomy field.
if (!($field = field_info_field($field_name)) || $field['type'] !== 'taxonomy_term_reference') {
// Error string. The JavaScript handler will realize this is not JSON and
// will display it as debugging information.
print t('Taxonomy field @field_name not found.', array('@field_name' => $field_name));
exit;
}
// The user enters a comma-separated list of tags. We only autocomplete the
// last tag.
$tags_typed = drupal_explode_tags($tags_typed);
$tag_last = drupal_strtolower(array_pop($tags_typed));
$term_matches = array();
if ($tag_last != '') {
if (!isset($langcode) || $langcode == LANGUAGE_NONE) {
$langcode = $GLOBALS['language_content']->language;
}
// Part of the criteria for the query come from the field's own settings.
$vocabulary = _entity_translation_taxonomy_reference_get_vocabulary($field);
$entity_type = 'taxonomy_term';
$query = new EntityFieldQuery();
$query->addTag('taxonomy_term_access');
$query->entityCondition('entity_type', $entity_type);
// If the Title module is enabled and the taxonomy term name is replaced for
// the current bundle, we can look for translated names, otherwise we fall
// back to the regular name property.
if (module_invoke('title', 'field_replacement_enabled', $entity_type, $vocabulary->machine_name, 'name')) {
$name_field = 'name_field';
$language_group = 0;
// Do not select already entered terms.
$column = 'value';
if (!empty($tags_typed)) {
$query->fieldCondition($name_field, $column, $tags_typed, 'NOT IN', NULL, $language_group);
}
$query->fieldCondition($name_field, $column, $tag_last, 'CONTAINS', NULL, $language_group);
$query->fieldLanguageCondition($name_field, array($langcode, LANGUAGE_NONE), NULL, NULL, $language_group);
}
else {
$name_field = 'name';
// Do not select already entered terms.
if (!empty($tags_typed)) {
$query->propertyCondition($name_field, $tags_typed, 'NOT IN');
}
$query->propertyCondition($name_field, $tag_last, 'CONTAINS');
}
// Select rows that match by term name.
$query->propertyCondition('vid', $vocabulary->vid);
$query->range(0, 10);
$result = $query->execute();
// Populate the results array.
$prefix = count($tags_typed) ? drupal_implode_tags($tags_typed) . ', ' : '';
$terms = !empty($result[$entity_type]) ? taxonomy_term_load_multiple(array_keys($result[$entity_type])) : array();
foreach ($terms as $tid => $term) {
$name = _entity_translation_taxonomy_label($term, $langcode);
$n = $name;
// Term names containing commas or quotes must be wrapped in quotes.
if (strpos($name, ',') !== FALSE || strpos($name, '"') !== FALSE) {
$n = '"' . str_replace('"', '""', $name) . '"';
}
$term_matches[$prefix . $n] = check_plain($name);
}
}
drupal_json_output($term_matches);
}
/**
* Returns the vocabulary enabled for the specified taxonomy reference field.
*
* @param array $field
* A field definition.
*
* @return object|null
* A vocabulary object or NULL if none is found.
*/
function _entity_translation_taxonomy_reference_get_vocabulary($field) {
$vocabulary = NULL;
if (!empty($field['settings']['allowed_values'])) {
$vids = array();
$vocabularies = taxonomy_vocabulary_get_names();
foreach ($field['settings']['allowed_values'] as $tree) {
$vids[] = $vocabularies[$tree['vocabulary']]->vid;
}
$vocabulary = taxonomy_vocabulary_load(reset($vids));
}
return $vocabulary;
}
/**
* Form element validate handler for taxonomy term autocomplete element.
*
* {@inheritdoc}
*
* @see taxonomy_autocomplete_validate()
*/
function entity_translation_taxonomy_autocomplete_validate($element, &$form_state) {
$value = array();
list($id) = entity_extract_ids($element['#entity_type'], $element['#entity']);
$is_new = !isset($id);
// This is the language of the parent entity, that we will be applying to new
// terms.
$parent_handler = entity_translation_get_handler($element['#entity_type'], $element['#entity']);
$langcode = !empty($form_state['entity_translation']['form_langcode']) ?
$form_state['entity_translation']['form_langcode'] : $parent_handler->getActiveLanguage();
// Handle in-place translation.
if (_entity_translation_taxonomy_autocomplete_translation_enabled($element, $form_state)) {
// The referenced terms cannot change, so we just need to collect their term
// identifiers. We also build a map of the corresponding deltas for later
// use.
$deltas = array();
foreach (element_children($element) as $delta) {
$tid = $element[$delta]['#tid'];
$deltas[$tid] = $delta;
$value[$delta]['tid'] = $tid;
}
// Save term translations.
$entity_type = 'taxonomy_term';
$name_field = 'name_field';
$source_langcode = $parent_handler->getSourceLanguage();
// This is a validation handler, so we must defer the actual save to the
// submit phase.
$terms_to_save = &$form_state['entity_translation']['taxonomy_autocomplete'][$element['#entity_type']][$id][$element['#field_name']];
foreach (taxonomy_term_load_multiple(array_keys($deltas)) as $term) {
// This is also the right context to perform validation.
$term_translation = $element[$deltas[$term->tid]]['#value'];
if (!$term_translation) {
$instance = field_info_instance($element['#entity_type'], $element['#field_name'], $element['#bundle']);
drupal_set_message(t('The translations for %field_label cannot be empty.', array('%field_label' => $instance['label'])), 'error', FALSE);
continue;
}
$handler = entity_translation_get_handler($entity_type, $term);
$translations = $handler->getTranslations();
$term_langcode = $handler->getLanguage();
$typed_langcode = $term_langcode != LANGUAGE_NONE ? $langcode : $term_langcode;
// Create a new translation in the active language, if it is missing.
if (!isset($translations->data[$typed_langcode]) && $typed_langcode != LANGUAGE_NONE) {
$translation = array(
'language' => $typed_langcode,
'source' => $source_langcode,
'uid' => $GLOBALS['user']->uid,
'status' => 1,
'created' => REQUEST_TIME,
'changed' => REQUEST_TIME,
);
$translation_values = array(
$name_field => array($typed_langcode => array(array('value' => $term_translation))),
);
$handler->setTranslation($translation, $translation_values);
$terms_to_save[] = $term;
}
// Otherwise we just update the existing translation, if it has changed.
// If the term is language-neutral, we just update its main value. This is
// expected to happen normally, but could when referencing existing terms.
elseif ($term_translation != _entity_translation_taxonomy_label($term, $typed_langcode)) {
$term->{$name_field}[$typed_langcode][0]['value'] = $term_translation;
$terms_to_save[] = $term;
}
}
}
// Autocomplete widgets do not send their tids in the form, so we must detect
// them here and process them independently.
elseif ($tags = $element['#value']) {
$entity_type = 'taxonomy_term';
$field = field_widget_field($element, $form_state);
$vocabulary = _entity_translation_taxonomy_reference_get_vocabulary($field);
$typed_tags = drupal_explode_tags($tags);
// Collect existing terms by name.
$existing_terms = array();
foreach (_entity_translation_taxonomy_autocomplete_widget_get_terms($element) as $term) {
$name = _entity_translation_taxonomy_label($term, $langcode);
$existing_terms[$name] = $term;
}
// Select terms that match by the (translated) name.
$query = new EntityFieldQuery();
$query->addTag('taxonomy_term_access');
$query->entityCondition('entity_type', $entity_type);
$query->propertyCondition('vid', $vocabulary->vid);
if ($langcode != LANGUAGE_NONE && module_invoke('title', 'field_replacement_enabled', $entity_type, $vocabulary->machine_name, 'name')) {
$language_group = 0;
// Do not select already entered terms.
$name_field = 'name_field';
$column = 'value';
$query->fieldCondition($name_field, $column, $typed_tags, NULL, NULL, $language_group);
// When we are creating a new entity, we cannot filter by active language,
// as that may have not be applied to the autocomplete query.
if (!$is_new) {
$query->fieldLanguageCondition($name_field, array($langcode, LANGUAGE_NONE), NULL, NULL, $language_group);
}
}
else {
$query->propertyCondition('name', $typed_tags);
}
$result = $query->execute();
// When we are creating a new entity, the language used for the autocomplete
// query is the current content language, so we should use that to update
// the map of existing terms.
if (!empty($result[$entity_type])) {
$typed_langcode = !$is_new ? $langcode : $GLOBALS['language_content']->language;
foreach (taxonomy_term_load_multiple(array_keys($result[$entity_type])) as $term) {
$name = _entity_translation_taxonomy_label($term, $typed_langcode);
$existing_terms[$name] = $term;
}
}
// Now collect the identifiers for the various terms and update the taxonomy
// reference field values.
foreach ($typed_tags as $delta => $typed_tag) {
// See if the term exists in the chosen vocabulary and return the tid.
// Otherwise create a new 'autocreate' term for insert/update.
if (isset($existing_terms[$typed_tag])) {
$term = $existing_terms[$typed_tag];
}
else {
$term = (object) array(
'tid' => 'autocreate',
'vid' => $vocabulary->vid,
'name' => $typed_tag,
'vocabulary_machine_name' => $vocabulary->machine_name,
);
$handler = entity_translation_get_handler($entity_type, $term);
$handler->setOriginalLanguage($langcode);
$handler->initTranslations();
}
$value[] = (array) $term;
}
}
form_set_value($element, $value, $form_state);
}
/**
* Term-specific implementation of hook_field_attach_submit().
*/
function entity_translation_taxonomy_term_field_attach_submit($entity_type, $entity, $form, &$form_state) {
// Finally save in-place translations
if (!empty($form_state['entity_translation']['taxonomy_autocomplete'])) {
foreach ($form_state['entity_translation']['taxonomy_autocomplete'] as $entity_type => $entity_type_data) {
foreach ($entity_type_data as $id => $field_name_data) {
foreach ($field_name_data as $field_name => $term_data) {
if (!is_array($term_data)) {
continue;
}
foreach ($term_data as $term) {
entity_translation_entity_save('taxonomy_term', $term);
}
}
}
}
}
}
@@ -7,9 +7,8 @@ dependencies[] = i18n
dependencies[] = i18n_menu
files[] = entity_translation_i18n_menu.test
; Information added by drupal.org packaging script on 2013-04-26
version = "7.x-1.0-beta2+33-dev"
; Information added by Drupal.org packaging script on 2019-11-24
version = "7.x-1.1"
core = "7.x"
project = "entity_translation"
datestamp = "1366981073"
datestamp = "1574630892"
@@ -58,11 +58,10 @@ function entity_translation_i18n_menu_node_presave($node) {
return;
}
// If we change from menu localization to translation sets while working on
// a translation (i.e. not the source node), then leave the source menu item
// intact and create a new one.
// When creating a new translation, leave the source menu item intact and
// create a new one.
$langcode = entity_language('node', $node);
if (!empty($node->menu) && $tset && $node->menu['language'] == LANGUAGE_NONE && !empty($source_langcode)) {
if (!empty($node->menu) && $tset && !empty($source_langcode)) {
$node->source_menu = menu_link_load($node->menu['mlid']);
unset($node->menu['mlid']);
}
@@ -165,7 +164,7 @@ function entity_translation_i18n_menu_form(&$form, &$form_state) {
$default = isset($source_menu['language']) && $source_menu['language'] != LANGUAGE_NONE;
$languages = language_list();
$handler = entity_translation_entity_form_get_handler($form, $form_state);
$langcode = $handler->getFormLanguage();
$langcode = $handler->getActiveLanguage();
$language_name = isset($languages[$langcode]) ? t($languages[$langcode]->name) : t('current');
$form['menu']['#multilingual'] = TRUE;
@@ -197,8 +196,8 @@ function entity_translation_i18n_menu_form_menu_edit_item_validate($form, &$form
$item = $form_state['values'];
// Localizable menu items should not be created when a translation set for the
// same path already exists.
if ($item['language'] == LANGUAGE_NONE) {
// same path already exists (exluding special paths starting by <).
if ($item['language'] == LANGUAGE_NONE && strpos($item['link_path'], '<') !== 0) {
$count = db_select('menu_links', 'ml')
->condition('ml.link_path', $item['link_path'])
->condition('ml.i18n_tsid', 0, '<>')
@@ -262,7 +261,7 @@ function entity_translation_i18n_menu_item($item) {
* @param $node
* A node object, with a menu item ($node->menu).
* @param $langcode
* The language into which the menu item should be translates.
* The language into which the menu item should be translated.
*/
function entity_translation_i18n_menu_node_menu_item_translate($node, $langcode) {
// Localization.
@@ -272,7 +271,7 @@ function entity_translation_i18n_menu_node_menu_item_translate($node, $langcode)
// used for the node menu form; i18n_menu_link_localize only localizes
// rendered properties 'title' and 'localized_options.attributes.title'.
$node->menu['link_title'] = $node->menu['title'];
$node->menu['options']['attributes']['title'] = $node->menu['localized_options']['attributes']['title'];
$node->menu['options']['attributes']['title'] = isset($node->menu['localized_options']['attributes']['title']) ? $node->menu['localized_options']['attributes']['title'] : '';
}
// Translation sets.
else {
@@ -288,7 +287,9 @@ function entity_translation_i18n_menu_node_menu_item_translate($node, $langcode)
// Make sure the menu item is not set to hidden; i18n_menu automatically
// hides any menu items not matching the current interface language.
$menu['hidden'] = FALSE;
if (!empty($menu)) {
$menu['hidden'] = FALSE;
}
}
// Replace the menu item with the translated version, or null if there is
@@ -368,6 +369,11 @@ function entity_translation_i18n_menu_entity_translation_upgrade($node, $transla
* Implements hook_entity_translation_delete().
*/
function entity_translation_i18n_menu_entity_translation_delete($entity_type, $entity, $langcode) {
// Make sure that we are working with an entity of type node.
if ($entity_type != 'node') {
return;
}
list($entity_id, , ) = entity_extract_ids($entity_type, $entity);
// Clean-up all menu module links that point to this node.
@@ -26,6 +26,7 @@ class EntityTranslationMenuTranslationTestCase extends EntityTranslationTestCase
$this->login($this->getAdminUser(array('administer menu')));
$this->addLanguage('en');
$this->addLanguage('es');
$this->addLanguage('it');
$this->configureContentType();
$this->configureMenu();
$this->enableUrlLanguageDetection();
@@ -45,13 +46,13 @@ class EntityTranslationMenuTranslationTestCase extends EntityTranslationTestCase
/**
* Create page with menu item.
*/
function createPage($link_title, $langcode) {
function createPage($link_title, $description, $langcode) {
$edit = array();
$edit['title'] = $this->randomName();
$edit['language'] = $langcode;
$edit['menu[enabled]'] = TRUE;
$edit['menu[link_title]'] = $link_title;
$edit['menu[description]'] = 'link language = ' . $langcode;
$edit['menu[description]'] = $description ? $description : 'link language = ' . $langcode;
$this->drupalPost('node/add/page', $edit, t('Save'));
$this->assertRaw(t('Basic page %title has been created.', array('%title' => $edit['title'])), t('Basic page created.'));
@@ -69,14 +70,16 @@ class EntityTranslationMenuTranslationTestCase extends EntityTranslationTestCase
/**
* Create a translation with menu item.
*/
function createTranslation($node, $link_title, $langcode, $use_tsets = FALSE) {
function createTranslation($node, $link_title, $description, $langcode, $use_tsets = FALSE) {
$this->drupalGet('node/' . $node->nid . '/edit/add/' . $node->language . '/' . $langcode);
$edit = array();
$edit['menu[enabled]'] = TRUE;
$edit['menu[link_title]'] = $link_title;
$edit['menu[description]'] = 'link language = ' . $langcode;
$edit['menu[tset]'] = $use_tsets;
$edit['menu[description]'] = $description ? $description : 'link language = ' . $langcode;
if ($use_tsets != NULL) {
$edit['menu[tset]'] = $use_tsets;
}
$this->drupalPost(NULL, $edit, t('Save'));
$this->drupalGet('node/' . $node->nid . '/translate');
@@ -122,23 +125,36 @@ class EntityTranslationMenuTranslationTestCase extends EntityTranslationTestCase
$this->assertText($link_title, 'New menu link title found: ' . $link_title);
}
/**
* Asserts a link with a given label and title is found in the page.
*/
function assertLinkWithTitleAttribute($label, $title) {
$links = $this->xpath('//a[normalize-space(text())=:label and normalize-space(@title)=:title]', array(
':label' => $label,
':title' => $title,
));
$this->assert(isset($links[0]));
}
/**
* Test if menu localization works.
*/
function testMenuLocalization() {
// Create Basic page in English.
$link_title_en = $this->randomName();
$node = $this->createPage($link_title_en, 'en');
$link_description_en = $this->randomName() . ' & htmlentity';
$node = $this->createPage($link_title_en, $link_description_en, 'en');
// Submit translation in Spanish.
$link_title_es = $this->randomName();
$node_translation = $this->createTranslation($node, $link_title_es, 'es');
$link_description_es = $this->randomName() . ' & htmlentity';
$node_translation = $this->createTranslation($node, $link_title_es, $link_description_es, 'es');
// Check menu links in both languages.
$this->get('en', '<front>');
$this->assertText($link_title_en);
$this->assertLinkWithTitleAttribute($link_title_en, $link_description_en);
$this->get('es', '<front>');
$this->assertText($link_title_es);
$this->assertLinkWithTitleAttribute($link_title_es, $link_description_es);
// Edit English menu link.
$link_title_en2 = $this->randomName();
@@ -146,15 +162,19 @@ class EntityTranslationMenuTranslationTestCase extends EntityTranslationTestCase
// Check that Spanish menu link has not changed.
$this->get('es', '<front>');
$this->assertText($link_title_es);
$this->assertLinkWithTitleAttribute($link_title_es, $link_description_es);
// Edit Spanish menu link.
$link_title_es2 = $this->randomName();
$this->editPage($node, $link_title_es, $link_title_es2, 'es');
// Check that English menu link has not changed.
// Check that English menu link has changed.
$this->get('en', '<front>');
$this->assertText($link_title_en2);
$this->assertLinkWithTitleAttribute($link_title_en2, $link_description_en);
// Check that Spanish menu link has changed.
$this->get('es', '<front>');
$this->assertLinkWithTitleAttribute($link_title_es2, $link_description_es);
// Delete Spanish translation and check that the respective menu item has
// been deleted as well.
@@ -169,11 +189,11 @@ class EntityTranslationMenuTranslationTestCase extends EntityTranslationTestCase
function testMenuLocalizationCustomSourceLanguage() {
// Create Basic page in Spanish.
$link_title_es = $this->randomName();
$node = $this->createPage($link_title_es, 'es');
$node = $this->createPage($link_title_es, NULL, 'es');
// Submit translation in English.
$link_title_en = $this->randomName();
$node_translation = $this->createTranslation($node, $link_title_en, 'en');
$node_translation = $this->createTranslation($node, $link_title_en, NULL, 'en');
// Check menu links in both languages.
$this->get('es', '<front>');
@@ -204,11 +224,11 @@ class EntityTranslationMenuTranslationTestCase extends EntityTranslationTestCase
function testMenuTranslation() {
// Create Basic page in English.
$link_title_en = $this->randomName();
$node = $this->createPage($link_title_en, 'en');
$node = $this->createPage($link_title_en, NULL, 'en');
// Submit translation in Spanish.
$link_title_es = $this->randomName();
$node_translation = $this->createTranslation($node, $link_title_es, 'es', TRUE);
$node_translation = $this->createTranslation($node, $link_title_es, NULL, 'es', TRUE);
// Check menu links in both languages.
$this->get('en', '<front>');
@@ -232,6 +252,17 @@ class EntityTranslationMenuTranslationTestCase extends EntityTranslationTestCase
$this->get('en', '<front>');
$this->assertText($link_title_en2);
// Add another translation, and check that other menu items are not
// affected. See https://drupal.org/node/1982140
$link_title_it = $this->randomName();
$node_translation = $this->createTranslation($node, $link_title_it, NULL, 'it', NULL);
// Check that Spanish and English menu links have not changed.
$this->get('es', '<front>');
$this->assertText($link_title_es2);
$this->get('en', '<front>');
$this->assertText($link_title_en2);
// Delete Spanish translation and check that the respective menu item has
// been deleted as well.
$this->removeTranslation($node, 'es');
@@ -34,8 +34,8 @@ function entity_translation_upgrade_start($types) {
*/
function entity_translation_upgrade_end($success, $results, $operations, $elapsed) {
if (!empty($results)) {
$message = format_plural($results, '1 node translation successfully upgraded.', '@count node translations successfully upgraded.');
watchdog('entity translation upgrade', '@count node translations successfully upgraded.', array('@count' => $results), WATCHDOG_INFO);
$message = format_plural($results['total'], '1 node translation successfully upgraded.', '@count node translations successfully upgraded.');
watchdog('entity translation upgrade', '@count node translations successfully upgraded.', array('@count' => $results['total']), WATCHDOG_INFO);
}
else {
$message = t('No node translation available for the upgrade.');
@@ -91,6 +91,10 @@ function entity_translation_upgrade_do($types, &$context) {
$node = $nodes[$nid];
$original = $nodes[$node->tnid];
$handler = entity_translation_get_handler('node', $original);
// Instantiate the data and original properties of the translations.
if (empty($handler->getTranslations()->data)) {
$handler->initTranslations();
}
if (!isset($instances[$node->type])) {
$instances[$node->type] = field_info_instances('node', $node->type);
@@ -185,7 +189,7 @@ function entity_translation_upgrade_do($types, &$context) {
$context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total'];
if ($context['finished'] >= 1) {
$context['results'] = $context['sandbox']['total'];
$context['results']['total'] = $context['sandbox']['total'];
}
}
}
@@ -0,0 +1,117 @@
<?php
/**
* Implements hook_drush_command().
*/
function entity_translation_upgrade_drush_command() {
$items = array();
$items['entity-translation-upgrade'] = array(
'description' => "Upgrades all nodes of the specified content type from Content Translation to Entity Translation.",
'arguments' => array(
'content_type' => 'Content type of nodes to be upgraded.',
),
'examples' => array(
'drush entity-translation-upgrade article' => 'Upgrades all nodes of content type "article" from Content Translation to Entity Translation.',
),
'aliases' => array('etu'),
'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_FULL,
);
return $items;
}
/**
* Implements hook_drush_help().
*
* @param
* A string with the help section
*
* @return
* A string with the help text for the entity-translation-upgrade command
*/
function entity_translation_upgrade_drush_help($section) {
switch ($section) {
case 'drush:entity-translation-upgrade':
return dt("Brief help for Drush command entity-translation-upgrade.");
case 'meta:entity_translation_upgrade:title':
return dt("Entity Translation Upgrade commands");
case 'meta:entity_translation_upgrade:summary':
return dt("Upgrading nodes to Entity Translation.");
}
}
/**
* Implements drush_hook_COMMAND().
*
* @param
* The content type of which the nodes shall be upgraded
*
* Runs the batch upgrading nodes of the specified content_type to Entity
* Translation. Lets user choose content type from a list, if argument has
* not been provided.
*/
function drush_entity_translation_upgrade($content_type = "") {
// Get all installed content types.
$available_types = array();
$available_types_chose = array();
$available_types_str = '';
foreach (node_type_get_types() as $type) {
$available_types[$type->type] = $type->type;
$available_types_chose[$type->type] = $type->name;
if (strlen($available_types_str) > 0) {
$available_types_str .= ', ';
}
$available_types_str .= $type->type;
}
// If argument content_type is empty, prompt user for content type.
if (!$content_type) {
$content_type = drush_choice($available_types_chose, dt('Choose the content type of the nodes to be upgraded to Entity Translation:'));
}
// Do content type argument checks.
if (!$content_type) {
return TRUE;
}
if (strlen($available_types_str) == 0) {
return drush_set_error(dt('Entity Translation Upgrade cannot run as no content type has been installed.'));
}
if (!in_array($content_type, $available_types)) {
return drush_set_error(dt('"@content_type" is not a valid content type machine name. Please use one of these installed content types as argument: @available_types_str.', array('@content_type' => $content_type, '@available_types_str' => $available_types_str)));
}
// Start batch to upgrade nodes of the specified content type.
$types = array($content_type => $content_type);
$batch = array(
'operations' => array(
array('entity_translation_upgrade_do', array($types)),
array('entity_translation_upgrade_complete', array()),
),
'finished' => 'entity_translation_upgrade_drush_end',
'file' => drupal_get_path('module', 'entity_translation_upgrade') . '/entity_translation_upgrade.admin.inc',
'progressive' => FALSE,
);
batch_set($batch);
drush_backend_batch_process();
}
/**
* This is the 'finished' batch callback, drush version.
*/
function entity_translation_upgrade_drush_end($success, $results, $operations, $elapsed) {
// Print result messages.
if (!empty($results)) {
$message = format_plural($results['total'], '1 node translation successfully upgraded.', '@count node translations successfully upgraded.');
$severity = 'ok';
watchdog('Entity Translation upgrade', '@count node translations successfully upgraded.', array('@count' => $results['total']), WATCHDOG_INFO);
}
else {
$message = t('No node translation available for the upgrade.');
$severity = 'warning';
}
drush_log($message, $severity);
}
@@ -3,10 +3,10 @@ description = Provides an upgrade path from node-based translation to field-base
package = Multilingual - Entity Translation
core = 7.x
dependencies[] = entity_translation
files[] = entity_translation_upgrade.test
; Information added by drupal.org packaging script on 2013-04-26
version = "7.x-1.0-beta2+33-dev"
; Information added by Drupal.org packaging script on 2019-11-24
version = "7.x-1.1"
core = "7.x"
project = "entity_translation"
datestamp = "1366981073"
datestamp = "1574630892"
@@ -0,0 +1,163 @@
<?php
/**
* @file
* Tests for Entity Translation module.
*/
/**
* Tests for the upgrade translation process.
*/
class EntityTranslationUpgradeTestCase extends EntityTranslationTestCase {
/**
* Return the test information.
*/
public static function getInfo() {
return array(
'name' => 'Translation upgrade',
'description' => 'Tests for the upgrade from Content Translation to Entity Translation.',
'group' => 'Entity translation',
'dependencies' => array(),
);
}
function setUp() {
parent::setUp('locale', 'translation', 'translation_test', 'entity_translation', 'entity_translation_upgrade');
$this->getAdminUser(array(
'toggle field translatability',
'administer entity translation',
));
$this->getTranslatorUser(array(
'translate content',
));
$this->login($this->getAdminUser());
$this->addLanguage('en');
$this->addLanguage('es');
$this->configureContentTypeForContentTranslation();
$this->enableUrlLanguageDetection();
$this->login($this->getTranslatorUser());
}
/**
* Configure the "Basic page" content type for entity translation tests.
*/
public function configureContentTypeForContentTranslation() {
// Configure the "Basic page" content type to use multilingual support with
// content translation.
$edit = array();
$edit['language_content_type'] = TRANSLATION_ENABLED;
$this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type'));
$this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Basic page')), t('Basic page content type has been updated.'));
}
/**
* Toggle body field's translatability.
*/
public function makeBodyFieldTranslatable() {
$edit = array();
$this->drupalGet('admin/structure/types/manage/page/fields/body');
$this->clickLink('Enable translation');
$this->drupalPost(NULL, array(), t('Confirm'));
$this->assertRaw(t('Data successfully processed.'), t('Body field have been made translatable.'));
}
/**
* @see TranslationTestCase::createPage
*/
function createContentTranslationPage($title, $body, $language = NULL) {
$edit = array();
$langcode = LANGUAGE_NONE;
$edit["title"] = $title;
$edit["body[$langcode][0][value]"] = $body;
if (!empty($language)) {
$edit['language'] = $language;
}
$this->drupalPost('node/add/page', $edit, t('Save'));
$this->assertRaw(t('Basic page %title has been created.', array('%title' => $title)), 'Basic page created.');
// Check to make sure the node was created.
$node = $this->drupalGetNodeByTitle($title);
$this->assertTrue($node, 'Node found in database.');
return $node;
}
/**
* @see TranslationTestCase::createTranslation
*/
function createContentTranslationTranslation($node, $title, $body, $language) {
$this->drupalGet('node/add/page', array('query' => array('translation' => $node->nid, 'target' => $language)));
$langcode = LANGUAGE_NONE;
$body_key = "body[$langcode][0][value]";
$this->assertFieldByXPath('//input[@id="edit-title"]', $node->title, "Original title value correctly populated.");
$this->assertFieldByXPath("//textarea[@name='$body_key']", $node->body[LANGUAGE_NONE][0]['value'], "Original body value correctly populated.");
$edit = array();
$edit["title"] = $title;
$edit[$body_key] = $body;
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertRaw(t('Basic page %title has been created.', array('%title' => $title)), 'Translation created.');
// Check to make sure that translation was successful.
$translation = $this->drupalGetNodeByTitle($title);
$this->assertTrue($translation, 'Node found in database.');
$this->assertTrue($translation->tnid == $node->nid, 'Translation set id correctly stored.');
return $translation;
}
/**
* Tests copying of source node's body value in the add translation form page.
*/
public function testUpgradeContentToEntityTranslation() {
// Create Basic page in English.
$node_title = $this->randomName();
$node_body = $this->randomName();
$node = $this->createContentTranslationPage($node_title, $node_body, 'en');
// Submit translation in Spanish.
$this->drupalGet('node/' . $node->nid . '/translate');
$node_translation_title = $this->randomName();
$node_translation_body = $this->randomName();
$node_translation = $this->createContentTranslationTranslation($node, $node_translation_title, $node_translation_body, 'es');
// Make Body field translatable before we run the upgrade.
$this->login($this->getAdminUser());
$this->makeBodyFieldTranslatable();
// Run the upgrade for all Page nodes.
$edit = array(
'types[page]' => 'page',
);
$this->drupalPost('admin/config/regional/entity_translation', $edit, t('Upgrade'));
// Switch to our translator user.
$this->login($this->getTranslatorUser());
// Check that the unpublished target node triggers a redirect.
$this->drupalGet('node/' . $node_translation->nid);
$headers = $this->drupalGetHeaders(TRUE);
list(, $status) = explode(' ', $headers[0][':status'], 3);
$this->assertEqual($status, 301, 'Expected response code was sent.');
$languages = language_list();
$this->assertEqual($this->getUrl(), url('node/' . $node->nid, array('absolute' => TRUE, 'language' => $languages['es'])), 'entity_translation_upgrade_redirect() redirected to expected URL.');
// Check that the body is displayed when the active language is English.
$this->drupalGet('node/' . $node->nid);
$this->assertRaw($node_body, t('Body field displayed correctly in the source language.'));
// Check that the translated body is displayed when the active language is Spanish.
$this->drupalGet('es/node/' . $node->nid);
$this->assertRaw($node_translation_body, t('Body field displayed correctly in the target language.'));
// Check that the edit forms are initialized correctly in the target language.
$this->drupalGet('node/' . $node->nid . '/edit');
$this->assertFieldByXPath("//textarea[@name='body[en][0][value]']", $node_body, "Body field correctly instantiated with the value of the source language.");
$this->drupalGet('es/node/' . $node->nid . '/edit');
$this->assertFieldByXPath("//textarea[@name='body[es][0][value]']", $node_translation_body, "Body field correctly instantiated with the value of the target language.");
}
}
@@ -14,6 +14,21 @@
*/
interface EntityTranslationHandlerInterface {
/**
* Injects the translation handler factory.
*/
public function setFactory(EntityTranslationHandlerFactory $factory);
/**
* Registers a child translation handler for the given entity.
*/
public function addChild($entity_type, $entity);
/**
* Removes a previously registered child translation handler.
*/
public function removeChild($entity_type, $entity);
/**
* Loads the translation data into the wrapped entity.
*/
@@ -62,6 +77,11 @@ interface EntityTranslationHandlerInterface {
*/
public function removeTranslations();
/**
* Removes all translations from the current revision.
*/
public function removeRevisionTranslations();
/**
* Initialize the language of the original field values.
*
@@ -78,6 +98,62 @@ interface EntityTranslationHandlerInterface {
*/
public function getLanguage();
/**
* Sets the active language.
*
* This is the language that determines which translation should be considered
* "active" for the wrapped entity. The "Entity Translation" module uses this
* information to implement the UI-based translation workflow. Other modules
* can rely on it to implement their own entity translation-related logic.
*
* This will affect which language is returned by the core "entity_language()"
* function.
*
* @param string $langcode
* The active language code.
*
* @see entity_language()
* @see entity_translation_language()
* @see ::getActiveLanguage()
*/
public function setActiveLanguage($langcode);
/**
* Returns the active language.
*
* @return string
* The active language for the wrapped entity.
*
* @see ::setActiveLanguage()
*/
public function getActiveLanguage();
/**
* Sets the active form language.
*
* @param string $langcode
* The active form language code.
*
* @deprecated in 7.x-1.0-beta6, will be removed before 7.x-1.0. Use
* ::setActiveLanguage() instead.
*
* @see ::setActiveLanguage()
*/
public function setFormLanguage($langcode);
/**
* Retrieves the active form language.
*
* @return string
* The active form language code.
*
* @deprecated in 7.x-1.0-beta6, will be removed before 7.x-1.0. Use
* ::getActiveLanguage() instead.
*
* @see ::getActiveLanguage()
*/
public function getFormLanguage();
/**
* Returns the translation object key for the wrapped entity type.
*/
@@ -89,7 +165,7 @@ interface EntityTranslationHandlerInterface {
public function getDefaultLanguage();
/**
* Sets the language of the orginal translation.
* Sets the language of the original translation.
*
* @param $langcode
* The language code of the original content values.
@@ -98,6 +174,9 @@ interface EntityTranslationHandlerInterface {
/**
* Returns TRUE if the entity is currently being translated.
*
* @deprecated This is no longer used and will be removed before the first
* stable release.
*/
public function isTranslating();
@@ -106,6 +185,9 @@ interface EntityTranslationHandlerInterface {
*
* @param $translating
* A boolean value.
*
* @deprecated This is no longer used and will be removed before the first
* stable release.
*/
public function setTranslating($translating);
@@ -114,6 +196,11 @@ interface EntityTranslationHandlerInterface {
*/
public function isRevision();
/**
* Return TRUE if the entity type supports revisions.
*/
public function isRevisionable();
/**
* Replaces the wrapped entity.
*
@@ -122,6 +209,22 @@ interface EntityTranslationHandlerInterface {
*/
public function setEntity($entity);
/**
* Returns the wrapped entity.
*
* @param return
* The wrapped entity.
*/
public function getEntity();
/**
* Returns the wrapped entity type.
*
* @param return
* The wrapped entity type.
*/
public function getEntityType();
/**
* Checks that the wrapped entity matches the give entity
*
@@ -225,16 +328,6 @@ interface EntityTranslationHandlerInterface {
*/
public function isAliasEnabled();
/**
* Sets the active form language.
*/
public function setFormLanguage($langcode);
/**
* Retrieves the active form language.
*/
public function getFormLanguage();
/**
* Sets the source language for the translation being created.
*/
@@ -301,20 +394,35 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
protected $entityInfo;
protected $entityId;
protected $bundle;
protected $revisionable;
private $entityForm;
private $translating;
private $outdated;
private $formLanguage;
private $sourceLanguage;
/**
* The translation handler factory.
*
* @var EntityTranslationHandlerFactory
*/
protected $factory;
private $pathScheme;
private $pathWildcard;
private $basePath;
private $editPath;
private $translatePath;
private $viewPath;
private $routerMap;
/**
* The translation handler hierarchy storage.
*
* @var array
*/
protected $children = array();
protected $entityForm;
protected $translating;
protected $outdated;
protected $activeLanguage;
protected $sourceLanguage;
protected $pathScheme;
protected $pathWildcard;
protected $basePath;
protected $editPath;
protected $translatePath;
protected $viewPath;
protected $routerMap;
/**
* Initializes an instance of the translation handler.
@@ -334,7 +442,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
$this->entityForm = FALSE;
$this->translating = FALSE;
$this->outdated = FALSE;
$this->formLanguage = FALSE;
$this->activeLanguage = FALSE;
$this->sourceLanguage = FALSE;
$this->pathScheme = 'default';
$this->routerMap = array();
@@ -357,17 +465,29 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
return;
}
$revisionable = self::isEntityTypeRevisionable($entity_type);
$revisions_ids = array();
foreach ($entities as $id => $entity) {
$entities[$id]->{$translations_key} = self::emptyTranslations();
if ($revisionable) {
list(, $revisions_id,) = entity_extract_ids($entity_type, $entity);
$revisions_ids[$id] = $revisions_id;
}
}
$results = db_select('entity_translation', 'et')
$table = $revisionable ? 'entity_translation_revision' : 'entity_translation';
$query = db_select($table, 'et')
->fields('et')
->condition('entity_type', $entity_type)
->condition('entity_id', array_keys($entities), 'IN')
->orderBy('entity_id')
->orderBy('created')
->execute();
->condition('entity_type', $entity_type);
if (!$revisionable) {
$query->condition('entity_id', array_keys($entities), 'IN');
}
else {
$query->condition('revision_id', $revisions_ids, 'IN');
}
$results = $query->execute();
foreach ($results as $row) {
$id = $row->entity_id;
@@ -394,6 +514,46 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
return $links;
}
/**
* @see EntityTranslationHandlerInterface::setFactory()
*/
public function setFactory(EntityTranslationHandlerFactory $factory) {
$this->factory = $factory;
}
/**
* @see EntityTranslationHandlerInterface::addChild()
*/
public function addChild($entity_type, $entity) {
if (!empty($this->factory)) {
$handler = $this->factory->getHandler($entity_type, $entity);
$handler->setActiveLanguage($this->getActiveLanguage());
$handler->setSourceLanguage($this->getSourceLanguage());
// Avoid registering more than one child handler for each entity.
$hid = $this->factory->getHandlerId($entity_type, $entity);
$this->children[$hid] = $handler;
}
}
/**
* @see EntityTranslationHandlerInterface::removeChild()
*/
public function removeChild($entity_type, $entity) {
if (!empty($this->factory)) {
$hid = $this->factory->getHandlerId($entity_type, $entity);
unset($this->children[$hid]);
}
}
/**
* Proxies the specified method invocation to a child translation handler.
*/
protected function notifyChildren($method, $args) {
foreach ($this->children as $handler) {
call_user_func_array(array($handler, $method), $args);
}
}
/**
* @see EntityTranslationHandlerInterface::loadTranslations()
*/
@@ -410,43 +570,14 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
* @see EntityTranslationHandlerInterface::saveTranslations()
*/
public function saveTranslations() {
// Delete and insert, rather than update, in case a value was added.
db_delete('entity_translation')
->condition('entity_type', $this->entityType)
->condition('entity_id', $this->entityId)
->execute();
$translations = $this->getTranslations();
if (count($translations->data)) {
global $user;
// Save current values.
$this->doSaveTranslations($translations, 'entity_translation');
$columns = array('entity_type', 'entity_id', 'language', 'source', 'uid', 'status', 'translate', 'created', 'changed');
$query = db_insert('entity_translation')->fields($columns);
// These values should overridde the translation ones as they are not
// supposed to change.
$overrides = array(
'entity_id' => $this->entityId,
'entity_type' => $this->entityType,
);
// These instead are just defaults.
$defaults = array(
'source' => '',
'uid' => $user->uid,
'translate' => 0,
'status' => 0,
'created' => REQUEST_TIME,
'changed' => REQUEST_TIME,
);
foreach ($translations->data as $langcode => $translation) {
$translation = $overrides + $translation + $defaults;
$query->values($translation);
}
$query->execute();
// Save revision values.
if ($this->isRevisionable()) {
$this->doSaveTranslations($translations, 'entity_translation_revision', TRUE);
}
// The translation handler interface decouples operations on translations at
@@ -476,6 +607,54 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
}
}
/**
* Saves entity translation records to the storage.
*/
protected function doSaveTranslations($translations, $table, $revision = FALSE) {
// Delete and insert, rather than update, in case a value was added.
$query = db_delete($table)
->condition('entity_type', $this->entityType)
->condition('entity_id', $this->entityId);
// If we are storing translations for the current revision or we are
// deleting the entity we should remove all translation data.
$langcode = $translations->original;
$hook = isset($translations->hook) ? $translations->hook : array();
if ($revision && $this->isRevisionable() && (empty($hook[$langcode]['hook']) || $hook[$langcode]['hook'] != 'delete')) {
$query->condition('revision_id', $this->revisionId);
}
$query->execute();
if (count($translations->data)) {
$columns = array('entity_type', 'entity_id', 'revision_id', 'language', 'source', 'uid', 'status', 'translate', 'created', 'changed');
$query = db_insert($table)->fields($columns);
// These values should override the translation ones as they are not
// supposed to change.
$overrides = array(
'entity_type' => $this->entityType,
'entity_id' => $this->entityId,
'revision_id' => $this->isRevisionable() ? $this->revisionId : $this->entityId,
);
// These instead are just defaults.
$defaults = array(
'source' => '',
'uid' => $GLOBALS['user']->uid,
'translate' => 0,
'status' => 0,
'created' => REQUEST_TIME,
'changed' => REQUEST_TIME,
);
foreach ($translations->data as $translation) {
$translation = $overrides + $translation + $defaults;
$query->values($translation);
}
$query->execute();
}
}
/**
* @see EntityTranslationHandlerInterface::getTranslations()
*/
@@ -495,6 +674,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
* @see EntityTranslationHandlerInterface::setTranslation()
*/
public function setTranslation($translation, $values = NULL) {
$args = func_get_args();
if (isset($translation['source']) && $translation['language'] == $translation['source']) {
throw new Exception('Invalid translation language');
}
@@ -525,11 +705,13 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
foreach (field_info_instances($this->entityType, $this->bundle) as $instance) {
$field_name = $instance['field_name'];
$field = field_info_field($field_name);
if ($field['translatable'] && isset($values[$field_name])) {
if ($field['translatable'] && isset($values[$field_name][$langcode])) {
$this->entity->{$field_name}[$langcode] = $values[$field_name][$langcode];
}
}
}
$this->notifyChildren(__FUNCTION__, $args);
}
/**
@@ -600,7 +782,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
if (empty($this->getTranslations()->data)) {
$this->initTranslations();
}
elseif (!empty($langcode) && !$this->isTranslating()) {
elseif (!empty($langcode)) {
$this->setOriginalLanguage($langcode);
}
}
@@ -612,6 +794,17 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
$this->removeTranslation(NULL);
}
/**
* @see EntityTranslationHandlerInterface::removeRevisionTranslations()
*/
public function removeRevisionTranslations() {
$translations_key = $this->getTranslationsKey();
$keys = array_keys($this->entity->{$translations_key}->data);
$values = array_fill(0, count($keys), array('hook' => 'delete_revision'));
$this->removeTranslation(NULL);
$this->entity->{$translations_key}->hook = array_combine($keys, $values);
}
/**
* @see EntityTranslationHandlerInterface::initOriginalTranslation()
*/
@@ -656,6 +849,55 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
}
}
/**
* {@inheritdoc}
*/
public function setActiveLanguage($langcode) {
// @todo To fully preserve BC, we proxy the call to the deprecated
// ::setFormLanguage method. This will keep things working even when it
// has been overridden. Inline its implementation here upon removal.
$this->setFormLanguage($langcode);
}
/**
* {@inheritdoc}
*/
public function getActiveLanguage() {
// @todo To fully preserve BC, we proxy the call to the deprecated
// ::getFormLanguage method. This will keep things working even when it
// has been overridden. Inline its implementation here upon removal.
return $this->getFormLanguage();
}
/**
* {@inheritdoc}
*/
public function setFormLanguage($langcode) {
$args = func_get_args();
$this->activeLanguage = $langcode;
$this->notifyChildren(__FUNCTION__, $args);
}
/**
* {@inheritdoc}
*/
public function getFormLanguage() {
if (!empty($this->activeLanguage)) {
return $this->activeLanguage;
}
// For new entities the active language should match the default language.
// The language stored with the entity itself (for example, $node->language)
// may not be reliable since the function creating the entity object will
// not know which language "Entity Translation" is configured to create the
// entity in.
elseif ($this->isNewEntity()) {
return $this->getDefaultLanguage();
}
else {
return $this->getLanguage();
}
}
/**
* @see EntityTranslationHandlerInterface::getLanguageKey()
*/
@@ -700,15 +942,19 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
* @see EntityTranslationHandlerInterface::setOriginalLanguage()
*/
public function setOriginalLanguage($langcode) {
$args = func_get_args();
$translations = $this->getTranslations();
if (isset($translations->original) && $translations->original != $langcode) {
$translations->data[$langcode] = $translations->data[$translations->original];
$translations->data[$langcode]['language'] = $langcode;
unset($translations->data[$translations->original]);
}
if (!isset($translations->original) || $translations->original != $langcode) {
if (isset($translations->original)) {
$translations->data[$langcode] = $translations->data[$translations->original];
$translations->data[$langcode]['language'] = $langcode;
unset($translations->data[$translations->original]);
}
$translations->original = $langcode;
$translations->original = $langcode;
$this->notifyChildren(__FUNCTION__, $args);
}
}
/**
@@ -732,6 +978,25 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
return FALSE;
}
/**
* @see EntityTranslationHandlerInterface::isRevisionable()
*/
public function isRevisionable() {
$result = FALSE;
if (!isset($this->revisionable)) {
$result = self::isEntityTypeRevisionable($this->entityType);
}
return $result;
}
/**
* Returns whether the entity type is revisionable.
*/
public static function isEntityTypeRevisionable($entity_type) {
$entity_info = entity_get_info($entity_type);
return variable_get('entity_translation_revision_enabled', FALSE) && !empty($entity_info['entity keys']['revision']);
}
/**
* @see EntityTranslationHandlerInterface::setEntity()
*/
@@ -744,8 +1009,27 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
$this->entity->{$translations_key} = self::emptyTranslations();
}
// Update bundle and entity id properties.
list($this->entityId, , $this->bundle) = entity_extract_ids($this->entityType, $this->entity);
// Update entity properties.
list($this->entityId, $this->revisionId, $this->bundle) = entity_extract_ids($this->entityType, $this->entity);
// Initialize the handler id if needed.
if (!empty($this->factory)) {
$this->factory->getHandlerId($this->entityType, $entity);
}
}
/**
* @see EntityTranslationHandlerInterface::getEntity()
*/
public function getEntity() {
return $this->entity;
}
/**
* @see EntityTranslationHandlerInterface::getEntityType()
*/
public function getEntityType() {
return $this->entityType;
}
/**
@@ -760,13 +1044,15 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
* @see EntityTranslationHandlerInterface::setOutdated()
*/
public function setOutdated($outdated) {
$args = func_get_args();
if ($outdated) {
$translations = $this->getTranslations();
foreach ($translations->data as $langcode => &$translation) {
if ($langcode != $this->getFormLanguage()) {
if ($langcode != $this->getActiveLanguage()) {
$translation['translate'] = 1;
}
}
$this->notifyChildren(__FUNCTION__, $args);
}
}
@@ -906,7 +1192,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
*/
public function getSharedFieldsAccess() {
$settings = entity_translation_settings($this->entityType, $this->bundle);
return ($settings['shared_fields_original_only'] == FALSE || $this->getLanguage() == $this->getFormLanguage()) &&
return ($settings['shared_fields_original_only'] == FALSE || $this->getLanguage() == $this->getActiveLanguage()) &&
(!entity_translation_workflow_enabled() || user_access('edit translation shared fields') || user_access("edit {$this->entityType} translation shared fields"));
}
@@ -917,25 +1203,13 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
return !empty($this->entityInfo['translation']['entity_translation']['alias']);
}
/**
* @see EntityTranslationHandlerInterface::setFormLanguage()
*/
public function setFormLanguage($langcode) {
$this->formLanguage = $langcode;
}
/**
* @see EntityTranslationHandlerInterface::getFormLanguage()
*/
public function getFormLanguage() {
return !empty($this->formLanguage) ? $this->formLanguage : $this->getLanguage();
}
/**
* @see EntityTranslationHandlerInterface::setSourceLanguage()
*/
public function setSourceLanguage($langcode) {
$args = func_get_args();
$this->sourceLanguage = $langcode;
$this->notifyChildren(__FUNCTION__, $args);
}
/**
@@ -966,7 +1240,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
public function entityForm(&$form, &$form_state) {
$this->entityForm = TRUE;
$translations = $this->getTranslations();
$form_langcode = $this->getFormLanguage();
$form_langcode = $this->getActiveLanguage();
$langcode = $this->getLanguage();
$is_translation = $this->isTranslationForm();
$new_translation = !isset($translations->data[$form_langcode]);
@@ -974,12 +1248,16 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
$languages = language_list();
$access = user_access('translate any entity') || user_access("translate $this->entityType entities");
// Store contextual information in the form state.
$form_state['entity_translation']['form_langcode'] = $form_langcode;
$form_state['entity_translation']['source_langcode'] = $this->getSourceLanguage();
// The only way to determine whether we are editing the original values is
// comparing form language and entity language. Since a language change
// might render impossible to make this check after form submission, we
// store the related information here.
$form_state['entity_translation']['is_translation'] = $is_translation;
// Adjust page title to specify the current language being edited, if we
// have at least one translation.
if ($form_langcode != LANGUAGE_NONE && (!$no_translations || $new_translation)) {
@@ -1019,7 +1297,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
// Add the entity language switcher.
$this->entityFormLanguageWidget($form, $form_state);
if ($is_translation) {
if ($is_translation && isset($form['actions']['delete'])) {
// Replace the delete button with the delete translation one.
if (!$new_translation) {
$weight = 100;
@@ -1082,7 +1360,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
'#disabled' => !$enabled,
);
$translate = !$new_translation && $translations->data[$form_langcode]['translate'];
$translate = !$new_translation && !empty($translations->data[$form_langcode]['translate']);
if (!$translate) {
$form['translation']['retranslate'] = array(
'#type' => 'checkbox',
@@ -1101,7 +1379,8 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
);
}
$name = $new_translation ? $GLOBALS['user']->name : user_load($translations->data[$form_langcode]['uid'])->name;
$translation_author = $new_translation ? $GLOBALS['user'] : user_load($translations->data[$form_langcode]['uid']);
$name = isset($translation_author->name) ? $translation_author->name : '';
$form['translation']['name'] = array(
'#type' => 'textfield',
'#title' => t('Authored by'),
@@ -1142,8 +1421,9 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
* Either remove access or add a translatability clue depending on the current
* user's "edit translation shared fields" permissions.
*/
public function entityFormSharedElements(&$element) {
static $ignored_types, $shared_labels, $access;
public function entityFormSharedElements(&$element, $access = NULL) {
static $ignored_types, $shared_labels;
if (!isset($ignored_types)) {
$ignored_types = array_flip(array('actions', 'value', 'hidden', 'vertical_tabs', 'token'));
}
@@ -1156,7 +1436,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
foreach (element_children($element) as $key) {
if (!isset($element[$key]['#type'])) {
$this->entityFormSharedElements($element[$key]);
$this->entityFormSharedElements($element[$key], $access);
}
else {
// Ignore non-widget form elements.
@@ -1183,7 +1463,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
public function entityFormLanguageWidget(&$form, &$form_state) {
if (entity_translation_enabled($this->entityType, $this->bundle)) {
$is_new = $this->isNewEntity();
$is_translation = !$is_new && !empty($form_state['entity_translation']['is_translation']);
$is_translation = !empty($form_state['entity_translation']['is_translation']);
$translations = $this->getTranslations();
$settings = entity_translation_settings($this->entityType, $this->bundle);
$languages = entity_translation_languages($this->entityType, $this->entity);
@@ -1191,8 +1471,8 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
foreach ($languages as $langcode => $language) {
// Disable languages for existing translations, so it is not possible to
// switch this entity to some language which is already in the translation
// set.
// switch this entity to some language which is already in the
// translation set.
if (!isset($translations->data[$langcode]) || empty($translations->data[$langcode]['source'])) {
$options[$langcode] = t($language->name);
}
@@ -1217,17 +1497,14 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
}
}
if (!empty($form['actions']['submit']['#submit'])) {
$submit = &$form['actions']['submit']['#submit'];
}
else {
if (!isset($form['#submit'])) {
$form['#submit'] = array();
}
$submit = &$form['#submit'];
}
array_unshift($submit, 'entity_translation_language_widget_submit');
// Prepend an empty form element to the form array so that we can update the
// form language before any other form handler has been called.
$form = array(
'entity_translation_entity_form_language_update' => array(
'#element_validate' => array('entity_translation_entity_form_language_update'),
'#entity_type' => $this->entityType,
),
) + $form;
}
/**
@@ -1241,6 +1518,15 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
* @see EntityTranslationHandlerInterface::entityFormValidate()
*/
public function entityFormValidate($form, &$form_state) {
// The locale module is creating the node object anew based on the form's
// submitted values, thus losing the relation with the currently assigned
// translation handler. By adding the 'entity_translation_handler_id' property to
// the $form_state['values'] array, we can persist that relation and utilize
// a correctly instantiated translation handler for the new node entity.
if (empty($form_state['values']['entity_translation_handler_id']) && field_has_translation_handler($this->getEntityType(), 'locale')) {
$form_state['values']['entity_translation_handler_id'] = $form_state[$this->getEntityType()]->entity_translation_handler_id;
}
if (!empty($form_state['values']['translation'])) {
$values = $form_state['values']['translation'];
// Validate the "authored by" field.
@@ -1260,28 +1546,36 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
protected function updateFormLanguage($form_state) {
// Update the form language as it might have changed. We exploit the
// validation phase to be sure to act as early as possible.
if (isset($form_state['values']['language']) && !$this->isTranslationForm()) {
$this->setFormLanguage($form_state['values'][$this->getLanguageKey()]);
$language_key = $this->getLanguageKey();
if (isset($form_state['values'][$language_key]) && !$this->isTranslationForm()) {
$langcode = $form_state['values'][$language_key];
$this->setActiveLanguage($langcode);
}
}
/**
* @see EntityTranslationHandlerInterface::entityFormLanguageWidgetSubmit()
*/
function entityFormLanguageWidgetSubmit($form, &$form_state) {
public function entityFormLanguageWidgetSubmit($form, &$form_state) {
if (!entity_translation_enabled($this->entityType, $this->bundle)) {
return;
}
$this->updateFormLanguage($form_state);
$form_langcode = $this->getFormLanguage();
$form_langcode = $this->getActiveLanguage();
foreach (field_info_instances($this->entityType, $this->bundle) as $instance) {
$field_name = $instance['field_name'];
$field = field_info_field($field_name);
$previous_langcode = $form[$field_name]['#language'];
if (isset($form[$field_name]['#language'])) {
$field = field_info_field($field_name);
$previous_langcode = $form[$field_name]['#language'];
// Handle a possible language change: new language values are inserted,
// previous ones are deleted.
if ($field['translatable'] && $previous_langcode != $form_langcode) {
$form_state['values'][$field_name][$form_langcode] = $form_state['values'][$field_name][$previous_langcode];
$form_state['values'][$field_name][$previous_langcode] = array();
// Handle a possible language change: new language values are inserted,
// previous ones are deleted.
if ($field['translatable'] && $previous_langcode != $form_langcode && isset($form_state['values'][$field_name][$previous_langcode])) {
$form_state['values'][$field_name][$form_langcode] = $form_state['values'][$field_name][$previous_langcode];
$form_state['values'][$field_name][$previous_langcode] = array();
}
}
}
}
@@ -1290,7 +1584,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
* @see EntityTranslationHandlerInterface::entityFormSubmit()
*/
public function entityFormSubmit($form, &$form_state) {
$form_langcode = $this->getFormLanguage();
$form_langcode = $this->getActiveLanguage();
$translations = $this->getTranslations();
$is_translation = !empty($form_state['entity_translation']['is_translation']);
$new_translation = !isset($translations->data[$form_langcode]);
@@ -1345,7 +1639,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
if (count($translations->data) > 0) {
$languages = language_list();
$form_langcode = $this->getFormLanguage();
$form_langcode = $this->getActiveLanguage();
$language_tabs = array();
if ($this->getSourceLanguage()) {
@@ -1406,16 +1700,19 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
/**
* Returns the title to be used for the entity form page.
*
* This may contain HTML markup and thus needs to be sanitized, since it will
* be used as page title as-is.
*/
protected function entityFormTitle() {
return $this->getLabel();
return check_plain($this->getLabel());
}
/**
* Returns TRUE if an entity translation is being edited.
*/
protected function isTranslationForm() {
return !$this->isNewEntity() && $this->getFormLanguage() != $this->getLanguage();
return !$this->isNewEntity() && $this->getActiveLanguage() != $this->getLanguage();
}
/**
@@ -1444,7 +1741,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
*
* @throws Exception
*/
private function initPathVariables() {
protected function initPathVariables() {
if (empty($this->pathScheme) || !isset($this->entityInfo['translation']['entity_translation']['path schemes'][$this->pathScheme])) {
throw new Exception("Cannot initialize entity translation path variables (invalid path scheme).");
}
@@ -1473,7 +1770,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa
if ($segment == $this->pathWildcard) {
$path_segments[$index] = $this->getEntityId();
}
elseif ($segment{0} == '%' && isset($this->routerMap[$index])) {
elseif ($segment[0] == '%' && isset($this->routerMap[$index])) {
$path_segments[$index] = $this->routerMap[$index];
}
}
@@ -92,7 +92,9 @@ class EntityTranslationNodeHandler extends EntityTranslationDefaultHandler {
if (!$this->isTranslationForm()) {
// Inherit entity authoring information for the original values.
$values['name'] = $form_state['values']['name'];
if (isset($form_state['values']['name'])) {
$values['name'] = $form_state['values']['name'];
}
if (!empty($form_state['values']['date'])) {
$values['created'] = $form_state['values']['date'];
}
@@ -15,6 +15,24 @@ class EntityTranslationTaxonomyTermHandler extends EntityTranslationDefaultHandl
parent::__construct('taxonomy_term', $entity_info, $entity);
}
/**
* @see EntityTranslationDefaultHandler::getLanguage()
*/
public function getLanguage() {
if (isset($this->entity->vid) && module_exists('i18n_taxonomy')) {
$mode = i18n_taxonomy_vocabulary_mode($this->entity->vid);
// We support also terms having no translation enabled, since they can
// just be language-aware.
if ($mode == I18N_MODE_NONE || $mode == I18N_MODE_ENTITY_TRANSLATION) {
$translations = $this->getTranslations();
if (!empty($translations->original)) {
return $translations->original;
}
}
}
return parent::getLanguage();
}
/**
* @see EntityTranslationDefaultHandler::entityForm()
*/
@@ -32,4 +50,5 @@ class EntityTranslationTaxonomyTermHandler extends EntityTranslationDefaultHandl
);
}
}
}
@@ -0,0 +1,137 @@
<?php
/**
* @file
* Translation handler factory for the Entity Translation module.
*/
/**
* Class implementing the entity translation handler factory.
*/
class EntityTranslationHandlerFactory {
/**
* A singleton instance of the factory.
*
* @var EntityTranslationHandlerFactory
*/
protected static $instance;
/**
* Counter used to generate handler ids for new entities.
*
* @var int
*/
protected static $newId = 1;
/**
* Handlers cache.
*
* @var array
*/
protected $handlers = array();
/**
* The last translation handler retrieved.
*
* @var EntityTranslationHandlerInterface
*/
protected $last;
/**
* An array of translation handler retrieved by type.
*
* @var array
*/
protected $lastByType = array();
/**
* Returns the singleton instance of the translation handler factory.
*
* @return EntityTranslationHandlerFactory
*/
public static function getInstance() {
if (!isset(self::$instance)) {
self::$instance = new EntityTranslationHandlerFactory();
}
return self::$instance;
}
/**
* Prevents the factory from being publicly instantiated.
*/
protected function __construct() {}
/**
* Translation handler factory.
*
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entity
* The entity to be translated. A bundle name may be passed to instantiate
* an empty entity.
*
* @return EntityTranslationHandlerInterface
* A class implementing EntityTranslationHandlerInterface.
*/
public function getHandler($entity_type, $entity) {
if (is_numeric($entity)) {
$entities = entity_load($entity_type, array($entity));
$entity = reset($entities);
}
elseif (is_string($entity)) {
$entity = entity_create_stub_entity($entity_type, array(NULL, NULL, $entity));
}
$id = $this->getHandlerId($entity_type, $entity);
if (!isset($this->handlers[$entity_type][$id])) {
$entity_info = entity_get_info($entity_type);
$class = $entity_info['translation']['entity_translation']['class'];
$handler = new $class($entity_type, $entity_info, $entity);
$handler->setFactory($this);
$this->handlers[$entity_type][$id] = $handler;
}
$this->last = $this->handlers[$entity_type][$id];
$this->lastByType[$entity_type] = $this->last;
$this->last->setEntity($entity);
return $this->last;
}
/**
* Retrieves the translation handler identifier for the given entity.
*
* @param $entity_type
* The type of the entity the translation handler will wrap.
* @param $entity
* The entity the translation handler will wrap.
*/
public function getHandlerId($entity_type, $entity) {
if (!isset($entity->entity_translation_handler_id)) {
list($id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity);
$revision_id = isset($revision_id) ? $revision_id : 0;
$bundle = isset($bundle) ? $bundle : $entity_type;
$entity->entity_translation_handler_id = $entity_type . '-' . $bundle . '-' . (!empty($id) ? 'eid-' . $id . '-' . $revision_id : 'new-' . self::$newId++);
}
return $entity->entity_translation_handler_id;
}
/**
* Returns the last translation handler retrieved.
*
* @param $entity_type
* (optional) The entity type of the translation handler. Defaults to the
* last one.
*
* @return EntityTranslationHandlerInterface
* A class implementing EntityTranslationHandlerInterface.
*/
public function getLastHandler($entity_type = NULL) {
if (isset($entity_type)) {
return isset($this->lastByType[$entity_type]) ? $this->lastByType[$entity_type] : NULL;
}
return $this->last;
}
}
@@ -0,0 +1,86 @@
<?php
/**
* @file
* MigrateTranslationEntityHandler class for entity_translation.
*/
/**
* Destination handler implementing translatable fields.
*/
class MigrateTranslationEntityHandler extends MigrateDestinationHandler {
/**
* Registers all entites as handled by this class.
*/
public function __construct() {
$this->registerTypes(array('entity'));
}
/**
* Handles Entity Translations.
*
* @param stdClass $entity
* @param stdClass $sourceRow
*/
public function prepare($entity, stdClass $row) {
$migration = Migration::currentMigration();
$entity_type = $migration->getDestination()->getEntityType();
// Only continue if the entity type + bundle combination is enabled for
// entity_translation.
if (entity_translation_enabled($entity_type, $entity)) {
// Get the entity_translation handler and load any existing translations.
$handler = entity_translation_get_handler($entity_type, $entity);
$handler->loadTranslations();
$original_language = $handler->getLanguage();
// Get the bundle of the entity.
list(, , $bundle) = entity_extract_ids($entity_type, $entity);
// We need to get all of the possible translations to create. So we look
// for any translatable fields.
$translatable_fields = array();
foreach (field_info_instances($entity_type, $bundle) as $instance) {
$field_name = $instance['field_name'];
$field = field_info_field($field_name);
if ($field['translatable']) {
$translatable_fields[] = $field_name;
}
}
// In those fields we look for translated values to build out an array
// of languages we need to set translations for.
$translate_languages = array();
foreach ($translatable_fields as $translatable_field) {
if (!empty($entity->{$translatable_field})) {
$field_languages = array_keys($entity->{$translatable_field});
foreach ($field_languages as $field_language) {
$translate_languages[$field_language] = $field_language;
}
}
}
// Remove the LANGUAGE_NONE results.
unset($translate_languages[LANGUAGE_NONE]);
// Remove the original language.
unset($translate_languages[$original_language]);
// Anything we're left with is a translation that should be set.
foreach ($translate_languages as $translate_language) {
if (!isset($entity->translations->data[$translate_language])) {
// Add the new translation and store it.
$handler->setTranslation(array(
'translate' => 0,
'status' => 1,
'language' => $translate_language,
'source' => $original_language,
'uid' => (empty($entity->uid)) ? 0 : $entity->uid,
'changed' => (empty($entity->changed)) ? REQUEST_TIME : $entity->changed,
'created' => (empty($entity->created)) ? REQUEST_TIME : $entity->created,
));
}
}
}
}
}
@@ -0,0 +1,142 @@
<?php
/**
* @file
* Plugin to provide access control on entities based on translations.
*/
/**
* CTools Plugin definition.
*/
$plugin = array(
'title' => t("Entity translation: translation exists"),
'description' => t('Control access by checking if a translation exists.'),
'callback' => 'entity_translation_translation_exists_ctools_access_check',
'default' => array('language' => array()),
'settings form' => 'entity_translation_translation_exists_ctools_access_settings',
'settings form submit' => 'entity_translation_translation_exists_ctools_access_settings_submit',
'summary' => 'entity_translation_translation_exists_ctools_access_summary',
'get child' => 'entity_translation_translation_exists_ctools_access_get_child',
'get children' => 'entity_translation_translation_exists_ctools_access_get_children',
);
/**
* Get a particular instance of this plugin.
*/
function entity_translation_translation_exists_ctools_access_get_child($plugin, $parent, $child) {
$plugins = &drupal_static(__FUNCTION__, array());
if (empty($plugins[$parent . ':' . $child])) {
$plugins[$parent . ':' . $child] = _entity_translation_translation_exists_ctools_access_definition($plugin, $parent, $child);
}
return $plugins[$parent . ':' . $child];
}
/**
* Get all children of this plugin.
*/
function entity_translation_translation_exists_ctools_access_get_children($plugin, $parent) {
$plugins = &drupal_static(__FUNCTION__, array());
if (!empty($plugins)) {
return $plugins;
}
$entities = entity_get_info();
foreach ($entities as $entity_type => $entity) {
$plugin = _entity_translation_translation_exists_ctools_access_definition($plugin, $parent, $entity_type);
$plugins[$parent . ':' . $entity_type] = $plugin;
}
return $plugins;
}
/**
* Plugin definition for one particular plugin child.
*/
function _entity_translation_translation_exists_ctools_access_definition($plugin, $parent, $entity_type) {
$entity = entity_get_info($entity_type);
$plugin['title'] = t('@entity: entity translation exists', array('@entity' => $entity['label']));
$plugin['keyword'] = $entity_type;
$plugin['description'] = t('Control access by @entity language', array('@entity' => $entity['label']));
$plugin['name'] = $parent . ':' . $entity_type;
$plugin['required context'] = new ctools_context_required(t(ucfirst($entity_type)), $entity_type);
return $plugin;
}
/**
* Settings form for the 'by node_language' access plugin
*/
function entity_translation_translation_exists_ctools_access_settings($form, &$form_state, $conf) {
$options = array(
ENTITY_TRANSLATION_LANGUAGE_CURRENT => t('Current content language'),
ENTITY_TRANSLATION_LANGUAGE_DEFAULT => t('Default site language'),
LANGUAGE_NONE => t('Language neutral'),
);
$options = array_merge($options, locale_language_list());
$form['settings']['language'] = array(
'#title' => t('Language'),
'#type' => 'checkboxes',
'#options' => $options,
'#description' => t('This rule will pass if any of these languages are present.'),
'#default_value' => $conf['language'],
);
return $form;
}
/**
* Grant access based on the which translations are available.
*/
function entity_translation_translation_exists_ctools_access_check($conf, $context) {
// Check that the context exists.
if (empty($context) || empty($context->data)) {
return FALSE;
}
$entity = $context->data;
$handler = entity_translation_get_handler($context->keyword, $entity);
if (!empty($handler)) {
$translations = $handler->getTranslations();
global $language_content;
foreach ($conf['language'] as $lang) {
if ($lang) {
switch ($lang) {
case ENTITY_TRANSLATION_LANGUAGE_CURRENT:
$lang = $language_content->language;
break;
case ENTITY_TRANSLATION_LANGUAGE_DEFAULT:
$lang = language_default('language');
break;
}
if (isset($translations->data[$lang]) && $translations->data[$lang]['status']) {
return TRUE;
}
}
}
}
return FALSE;
}
/**
* Provide a summary description based upon the checked node_languages.
*/
function entity_translation_translation_exists_ctools_access_summary($conf, $context) {
$languages = array(
ENTITY_TRANSLATION_LANGUAGE_CURRENT => t('Current site content language'),
ENTITY_TRANSLATION_LANGUAGE_DEFAULT => t('Default site language'),
LANGUAGE_NONE => t('Language neutral'),
);
$languages = array_merge($languages, locale_language_list());
if (!isset($conf['language'])) {
$conf['language'] = array();
}
$names = array();
foreach (array_filter($conf['language']) as $language) {
$names[] = $languages[$language];
}
if (empty($names)) {
return t('@identifier is in any language', array('@identifier' => $context->identifier));
}
return format_plural(count($names), '@languages translation exists for @identifier', '@languages translations exists for identifier', array('@languages' => implode(', ', $names), '@identifier' => $context->identifier));
}
@@ -4,11 +4,11 @@ core = 7.x
package = Testing
hidden = TRUE
dependencies[] = entity_translation
files[] = entity_translation_test.module
; Information added by drupal.org packaging script on 2013-04-26
version = "7.x-1.0-beta2+33-dev"
; Information added by Drupal.org packaging script on 2019-11-24
version = "7.x-1.1"
core = "7.x"
project = "entity_translation"
datestamp = "1366981073"
datestamp = "1574630892"
@@ -5,3 +5,34 @@
* Installation functionality for Entity Translation testing module.
*/
/**
* Implements hook_install().
*/
function entity_translation_test_install() {
// Create a simple text field, attached to taxonomy_terms.
field_info_cache_clear();
$field = array(
'field_name' => 'field_simple_text',
'type' => 'text',
'cardinality' => 1,
);
field_create_field($field);
$instance = array(
'field_name' => $field['field_name'],
'label' => ucfirst(str_replace('_', ' ', $field['field_name'])),
'entity_type' => 'taxonomy_term',
'bundle' => 'tags',
'widget' => array(
'type' => 'text_textfield',
),
'display' => array(
'default' => array(
'type' => 'text_default',
),
),
);
field_create_instance($instance);
}
@@ -5,6 +5,18 @@
* Testing functionality for Entity Translation module.
*/
/**
* Implements hook_form_alter().
*/
function entity_translation_test_form_alter(&$form, &$form_state) {
if ($info = entity_translation_edit_form_info($form, $form_state)) {
$handler = entity_translation_get_handler($info['entity type'], $info['entity']);
if (entity_translation_enabled($info['entity type'], $info['entity'])) {
$form_state['no_cache'] = $handler->isNewEntity() && variable_get('entity_translation_test_disable_form_cache', FALSE);
}
}
}
/**
* Implements hook_entity_translation_insert().
*/
@@ -182,7 +182,7 @@ function entity_translation_views_data() {
);
$data['entity_translation']['translation_exists'] = array(
'title' => t('Translation exists'),
'help' => t('Link to translation overview page.'),
'help' => t('Determines if a translation exists for a particular translation.'),
'filter' => array(
'handler' => 'entity_translation_handler_filter_translation_exists',
),
@@ -224,4 +224,24 @@ function entity_translation_views_data_alter(&$data) {
);
}
}
// Expose all translatable fields, using a handler based off Views' default
// field handler, that allows users to select the language to display the
// field in.
foreach (field_info_fields() as $field) {
if ($field['translatable'] && $field['storage']['type'] == 'field_sql_storage') {
// Set defaults and just change the handler, title, group, and help.
$defaults = field_views_field_default_views_data($field);
foreach ($defaults as $table_name => $table_data) {
if (isset($data[$table_name][$field['field_name']]) && isset($table_data[$field['field_name']])) {
$field_title = $table_data[$field['field_name']]['title'];
$table_data[$field['field_name']]['title'] = t('!title: translated', array('!title' => $field_title));
$table_data[$field['field_name']]['group'] = t('Entity translation');
$table_data[$field['field_name']]['help'] .= ' ' . t('Show the field !title translated into a specified language', array('!title' => $field_title));
$table_data[$field['field_name']]['field']['handler'] = 'entity_translation_handler_field_field';
$data[$table_name][$field['field_name'] . '_et'] = $table_data[$field['field_name']];
}
}
}
}
}
@@ -0,0 +1,65 @@
<?php
/**
* @file
* This file contains a field handler for entity translation that shows field
* content translated into a specified language.
*/
class entity_translation_handler_field_field extends views_handler_field_field {
function option_definition() {
$options = parent::option_definition();
$options['language'] = array(
'default' => '***CURRENT_LANGUAGE***',
);
return $options;
}
function options_form(&$form, &$form_state) {
parent::options_form($form, $form_state);
$languages = array(
'***CURRENT_LANGUAGE***' => t("Current user's language"),
'***DEFAULT_LANGUAGE***' => t("Default site language"),
LANGUAGE_NONE => t('Language neutral'),
);
$languages = array_merge($languages, locale_language_list());
$form['language'] = array(
'#type' => 'select',
'#title' => t('Language'),
'#options' => $languages,
'#default_value' => $this->options['language'],
'#description' => t('Select the language to display this field in')
);
}
/**
* Overrides parent::field_language, retrieving the language from the handler
* options.
*/
function field_language($entity_type, $entity) {
global $language_content;
if (field_is_translatable($entity_type, $this->field_info)) {
$default_language = language_default('language');
$language = str_replace(array('***CURRENT_LANGUAGE***', '***DEFAULT_LANGUAGE***'),
array($language_content->language, $default_language),
$this->options['language']);
// Give the Field Language API a chance to fallback to a different language
// (or LANGUAGE_NONE), in case the field has no data for the selected language.
// field_view_field() does this as well, but since the returned language code
// is used before calling it, the fallback needs to happen explicitly.
$language = field_language($entity_type, $entity, $this->field_info['field_name'], $language);
return $language;
}
else {
return LANGUAGE_NONE;
}
}
}
@@ -1,7 +1,8 @@
<?php
/**
* @file
* Contains the relationship plugin for relating entities to translation metadata.
* Contains a views plugin for relating entities to translation metadata.
*/
/**
@@ -27,7 +28,10 @@ class entity_translation_handler_relationship extends views_handler_relationship
$alias = $def['table'] . '_' . $this->table;
// We need to add a condition on entity type to the join to avoid getting
// relationships to entities with other types.
$join->extra = "$alias.entity_type = '{$def['entity type']}'";
$join->extra = array(
array('field' => 'entity_type', 'value' => $def['entity type']),
);
$this->alias = $this->query->add_relationship($alias, $join, 'entity_translation', $this->relationship);
}
}
@@ -20,7 +20,7 @@ For support, please create a support request for this module's project:
Support questions by email to the module maintainer will be simply ignored. Use the issue tracker.
Now if you want professional (paid) support the module maintainer may be available occassionally.
Now if you want professional (paid) support the module maintainer may be available occasionally.
Drop me a message to check availability and hourly rates, http://reyero.net/en/contact
====================================================================
@@ -4,15 +4,12 @@ dependencies[] = locale
dependencies[] = variable
package = Multilingual - Internationalization
core = 7.x
files[] = i18n.install
files[] = i18n_object.inc
files[] = i18n.test
configure = admin/config/regional/i18n
; Information added by drupal.org packaging script on 2013-01-13
version = "7.x-1.8"
; Information added by Drupal.org packaging script on 2020-06-17
version = "7.x-1.27"
core = "7.x"
project = "i18n"
datestamp = "1358075001"
datestamp = "1592373390"
@@ -22,12 +22,21 @@ function i18n_uninstall() {
}
/**
* Add fields to schema if they don't exist
* Add fields to the schema if they don't exist.
*
* @param string $table
* The name of the database table.
* @param array $fields
* The list of database fields to create.
* @param boolean $force_rebuild_schema
* Whether to force drupal_get_schema() to rebuild the schema. This may be
* necessary when additional implementations of hook_schema_alter() have
* become available since the schema was originally built.
*/
function i18n_install_create_fields($table, $fields) {
function i18n_install_create_fields($table, $fields, $force_rebuild_schema = FALSE) {
static $schema;
// Do not force schema refresh more than once per request.
$schema = drupal_get_schema($table, !isset($schema));
$rebuild_schema = !isset($schema) || $force_rebuild_schema;
$schema = drupal_get_schema($table, $rebuild_schema);
foreach ($fields as $field) {
if (!empty($schema['fields'][$field])) {
if (!db_field_exists($table, $field)) {
@@ -198,7 +198,8 @@ function i18n_language_field_extra() {
*/
function i18n_language_list($field = 'name', $mode = NULL) {
$mode = isset($mode) ? $mode : variable_get('i18n_language_list', I18N_LANGUAGE_ENABLED);
return locale_language_list($field, I18N_LANGUAGE_EXTENDED & $mode);
$all = I18N_LANGUAGE_EXTENDED & $mode;
return locale_language_list($field, $all);
}
/**
@@ -403,9 +404,17 @@ function i18n_get_object($type, $key, $object = NULL) {
$index = is_array($key) ? implode(':', $key) : $key;
if (!isset($cache[$type][$index])) {
$class = i18n_object_info($type, 'class', 'i18n_object_wrapper');
$cache[$type][$index] = new $class($type, $key, $object);
$object_wrapper = new $class($type, $key, $object);
// Do not cache object with empty index.
if (!empty($index)) {
$cache[$type][$index] = $object_wrapper;
}
}
return $cache[$type][$index];
else {
$object_wrapper = $cache[$type][$index];
}
return $object_wrapper;
}
/**
@@ -498,9 +507,12 @@ function i18n_object_translate_access($type, $object) {
*
* @param $path
* Path to get translations for or '<front>' for front page.
* @param $check_access
* Whether to check access to paths, defaults to TRUE
*/
function i18n_get_path_translations($path) {
function i18n_get_path_translations($path, $check_access = TRUE) {
$translations = &drupal_static(__FUNCTION__);
if (!isset($translations[$path])) {
$translations[$path] = array();
foreach (module_implements('i18n_translate_path') as $module) {
@@ -510,10 +522,31 @@ function i18n_get_path_translations($path) {
$translations[$path] += $translated;
}
}
// Add access information if not there.
foreach ($translations[$path] as $langcode => &$info) {
if (!isset($info['access'])) {
$item = menu_get_item($info['href']);
// If no menu item, it may be an external URL, we allow access.
$info['access'] = $item ? !empty($item['access']) : TRUE;
}
}
// Chance for altering the results.
drupal_alter('i18n_translate_path', $translations[$path], $path);
}
return $translations[$path];
if ($check_access) {
return array_filter($translations[$path], '_i18n_get_path_translations_access');
}
else {
return $translations[$path];
}
}
/**
* Helper function to check access to path translation.
*/
function _i18n_get_path_translations_access($path) {
return $path['access'];
}
/**
@@ -11,7 +11,7 @@ class Drupali18nTestCase extends DrupalWebTestCase {
function setUpLanguages($admin_permissions = array()) {
// Setup admin user.
$this->admin_user = $this->drupalCreateUser(array_merge(array('bypass node access', 'administer nodes', 'administer languages', 'administer content types', 'administer blocks', 'access administration pages', 'translate interface'), $admin_permissions));
$this->admin_user = $this->drupalCreateUser(array_merge(array('bypass node access', 'administer nodes', 'administer languages', 'administer content types', 'administer fields', 'administer blocks', 'access administration pages', 'translate interface'), $admin_permissions));
$this->drupalLogin($this->admin_user);
@@ -144,12 +144,14 @@ class Drupali18nTestCase extends DrupalWebTestCase {
}
/**
* Create a "Basic page" in the specified language.
*
* Create a node of the specified type in the specified language.
* @param $type
* The node type.
* @param $title
* Title of basic page in specified language.
* Title of node in specified language.
* @param $body
* Body of basic page in specified language.
* Body of node in specified language.
* @param $langcode
* Language code.
*/
@@ -159,7 +161,9 @@ class Drupali18nTestCase extends DrupalWebTestCase {
$edit["body[$lang][0][value]"] = $body;
$edit['language'] = $langcode;
$this->drupalPost('node/add/' . $type, $edit, t('Save'));
$this->assertRaw(t('Basic @type %title has been created.', array('@type' => $type, '%title' => $title)), t('Basic page created.'));
$info = node_type_load($type);
$message = t('@name %title has been created.', array('@name' => $info->name, '%title' => $title));
$this->assertRaw($message);
// Check to make sure the node was created.
$node = $this->drupalGetNodeByTitle($title);
@@ -169,15 +173,14 @@ class Drupali18nTestCase extends DrupalWebTestCase {
}
/**
* Create a translation for the specified basic page in the specified
* language.
* Create a translation for the specified node in the specified language.
*
* @param $node
* The basic page to create translation for.
* @param $title
* Title of basic page in specified language.
* Title of node in specified language.
* @param $body
* Body of basic page in specified language.
* Body of node in specified language.
* @param $language
* Language code.
*/
@@ -196,8 +199,9 @@ class Drupali18nTestCase extends DrupalWebTestCase {
$edit["title"] = $title;
$edit[$body_key] = $body;
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertRaw(t('Basic page %title has been created.', array('%title' => $title)), t('Translation created.'));
$info = node_type_load($node->type);
$message = t('@name %title has been created.', array('@name' => $info->name, '%title' => $title));
$this->assertRaw($message);
// Check to make sure that translation was successful.
$translation = $this->drupalGetNodeByTitle($title);
$this->assertTrue($translation, t('Node found in database.'));
@@ -358,6 +362,7 @@ class Drupali18nTestCase extends DrupalWebTestCase {
function resetCaches() {
drupal_static_reset('locale_url_outbound_alter');
drupal_static_reset('language_list');
drupal_language_initialize();
}
/**
@@ -0,0 +1,92 @@
CONTENTS OF THIS FILE
---------------------
* Introduction
* Requirements
* Recommended modules
* Installation
* Configuration
* Troubleshooting
* Maintainers
INTRODUCTION
------------
The Block languages module, part of the Internationalization
(https://www.drupal.org/project/i18n) package, allows the user to configure
for which languages each block is visible.
* For a full description of the module,
visit https://www.drupal.org/node/1279698.
* To submit bug reports and feature suggestions, or to track changes visit
https://www.drupal.org/project/issues/i18n.
REQUIREMENTS
------------
This module requires the following module:
* Internationalization - https://www.drupal.org/project/i18n
RECOMMENDED MODULES
-------------------
* Internationalization Views - https://www.drupal.org/project/i18nviews
* Language Icons - https://www.drupal.org/project/languageicons
* Translation Overview - https://www.drupal.org/project/translation_overview
* Localization Client - https://www.drupal.org/project/l10n_client
* Internationalization contributions -
https://www.drupal.org/project/i18n_contrib
INSTALLATION
------------
This is a submodule of the Internationalization module. Install the
Internationalization module as you would normally install a contributed Drupal
module. Visit https://www.drupal.org/node/895232 for further information.
CONFIGURATION
-------------
The settings for visibility per language are provided under Visibility
Settings via the Languages tab when configuring a block.
The Languages tab also provides a setting for whether the block is translatable.
For custom blocks, the block title and the block content will be translatable.
For blocks defined by modules, only the block title will be translatable. If
"Make this block translatable" is selected, a Translate tab will appear for that
block. This tab provides a UI for adding translations of the block in each
available language.
TROUBLESHOOTING
---------------
Conflicts with Context
The Block languages module conflicts with the Context module, which alters how
blocks are rendered. This issue can be tracked in the Internationalization
issue queue: http://drupal.org/node/1343044
String Errors
The user must allow your used string format to be translated on
admin/config/regional/i18n/strings or you are going to have an error message
like "The string blocks:block:1:body for textgroup blocks is not allowed for
translation because of its text format."
MAINTAINERS
-----------
* Jose Reyero - https://www.drupal.org/u/jose-reyero
* Florian Weber (webflo) - https://www.drupal.org/u/webflo
* Peter Philipp - https://www.drupal.org/u/das-peter
* Joseph Olstad - https://www.drupal.org/u/joseph.olstad
* Nathaniel Catchpole - https://www.drupal.org/u/catch
@@ -8,9 +8,8 @@ files[] = i18n_block.inc
files[] = i18n_block.test
; Information added by drupal.org packaging script on 2013-01-13
version = "7.x-1.8"
; Information added by Drupal.org packaging script on 2020-06-17
version = "7.x-1.27"
core = "7.x"
project = "i18n"
datestamp = "1358075001"
datestamp = "1592373390"
@@ -37,6 +37,17 @@ function i18n_block_menu() {
return $items;
}
/**
* Implements hook_permission().
*/
function i18n_block_permission() {
return array(
'translate blocks' => array(
'title' => t('Translate Blocks'),
),
);
}
/**
* Implement hook_menu_alter().
*
@@ -59,7 +70,7 @@ function i18n_block_menu_alter(&$items) {
*/
function i18n_block_translate_tab_access($module, $delta) {
$block = block_load($module, $delta);
return user_access('translate interface') && isset($block) && ($block->i18n_mode == I18N_MODE_LOCALIZE);
return (user_access('translate interface') || user_access('translate blocks')) && $block && isset($block->i18n_mode) && ($block->i18n_mode == I18N_MODE_LOCALIZE);
}
/**
@@ -226,7 +237,7 @@ function i18n_block_form_block_admin_configure_alter(&$form, &$form_state, $form
'#options' => i18n_language_list(),
'#description' => t('If no language is selected, block will show regardless of language.'),
);
if (user_access('translate interface')) {
if (user_access('translate interface') || user_access('translate blocks')) {
$form['actions']['translate'] = array(
'#type' => 'submit',
'#name' => 'save_translate',
@@ -72,15 +72,16 @@ class i18nBlocksTestCase extends Drupali18nTestCase {
$this->clickLink(t('translate'));
// Title is a textarea, body is a text_format.
$this->assertFieldByName('strings[blocks:block:' . $box2['delta'] . ':title]', $translations['title']['es']);
$this->assertFieldByName('strings[blocks:block:' . $box2['delta'] . ':body]', $translations['body']['es']);
$this->assertFieldByName('strings[blocks:block:' . $box2['delta'] . ':body][value]', $translations['body']['es']);
// Update the translation.
$translations['title']['es'] = $this->randomName(10);
$translations['body']['es'] = $this->randomName(20);
$edit = array(
'strings[blocks:block:' . $box2['delta'] . ':title]' => $translations['title']['es'],
'strings[blocks:block:' . $box2['delta'] . ':body]' => $translations['body']['es'],
'strings[blocks:block:' . $box2['delta'] . ':body][value]' => $translations['body']['es'],
);
$this->drupalPost(NULL, $edit, t('Save translation'));
$this->i18nAssertTranslations($translations['title'], '', 'Updated block title translation displayed.');
@@ -0,0 +1,76 @@
CONTENTS OF THIS FILE
---------------------
* Introduction
* Requirements
* Recommended modules
* Installation
* Configuration
* Maintainers
INTRODUCTION
------------
The Contact translation module, part of the Internationalization
(https://www.drupal.org/project/i18n) package, helps with the multilingual
configuration of the site contact forms. This module makes contact categories
and replies available for translation.
* For a full description of the module, visit
https://www.drupal.org/node/1396984.
* To submit bug reports and feature suggestions, or to track changes,
visit https://www.drupal.org/project/issues/i18n.
REQUIREMENTS
------------
This module requires the following module:
* Internationalization - https://www.drupal.org/project/i18n
RECOMMENDED MODULES
-------------------
* Internationalization Views - https://www.drupal.org/project/i18nviews
* Language Icons - https://www.drupal.org/project/languageicons
* Translation Overview - https://www.drupal.org/project/translation_overview
* Localization Client - https://www.drupal.org/project/l10n_client
* Internationalization contributions -
https://www.drupal.org/project/i18n_contrib
INSTALLATION
------------
This is a submodule of the Internationalization module. Install the
Internationalization module as you would normally install a contributed Drupal
module. Visit https://www.drupal.org/node/895232 for further information.
CONFIGURATION
-------------
1. Enable the Contact translation module included with the Internationalization
module.
2. Go to Administration > Structure > Contact form.
3. Select edit for the form to configure.
4. Select the Translate tab.
5. Select the translate link for a language.
6. Translate the Category and Auto-reply text.
7. Select Save translation.
8. Repeat steps 5 to 7 for each language.
9. Repeat steps 2 to 8 for all forms.
MAINTAINERS
-----------
* Jose Reyero - https://www.drupal.org/u/jose-reyero
* Florian Weber (webflo) - https://www.drupal.org/u/webflo
* Peter Philipp - https://www.drupal.org/u/das-peter
* Joseph Olstad - https://www.drupal.org/u/joseph.olstad
* Nathaniel Catchpole - https://www.drupal.org/u/catch
@@ -5,9 +5,8 @@ dependencies[] = i18n_string
package = Multilingual - Internationalization
core = 7.x
; Information added by drupal.org packaging script on 2013-01-13
version = "7.x-1.8"
; Information added by Drupal.org packaging script on 2020-06-17
version = "7.x-1.27"
core = "7.x"
project = "i18n"
datestamp = "1358075001"
datestamp = "1592373390"
@@ -0,0 +1,78 @@
CONTENTS OF THIS FILE
---------------------
* Introduction
* Requirements
* Recommended modules
* Installation
* Configuration
* Maintainers
INTRODUCTION
------------
The Field translation module, part of the Internationalization
(https://www.drupal.org/project/i18n) package, allows for translation of text
associated with a field's settings including the label, help text, default
value, and list options.
* For a full description of the module, visit this page
https://www.drupal.org/node/1279346.
* To submit bug reports and feature suggestions, or to track changes visit
https://www.drupal.org/project/issues/i18n.
REQUIREMENTS
------------
This module requires the following module:
* Internationalization - https://www.drupal.org/project/i18n
RECOMMENDED MODULES
-------------------
* Internationalization Views - https://www.drupal.org/project/i18nviews
* Language Icons - https://www.drupal.org/project/languageicons
* Translation Overview - https://www.drupal.org/project/translation_overview
* Localization Client - https://www.drupal.org/project/l10n_client
* Internationalization contributions -
https://www.drupal.org/project/i18n_contrib
INSTALLATION
------------
This is a submodule of the Internationalization module. Install the
Internationalization module as you would normally install a contributed Drupal
module. Visit https://www.drupal.org/node/895232 for further information.
CONFIGURATION
-------------
1. Enable the Field translation module included with Internationalization.
2. Go to Administration > Configuration > Regional and language > Translate
interface.
3. Select the Filter Translatable Strings field set and limit search to fields.
4. Edit the desired field and Save.
For the translation to be displayed, the user needs to use some of the Field
Formatters provided by this module whose name usually ends up in 'translated'.
For most core fields it is Default translated.
Note: The Field Translation module does not provide content translation for
fields. This functionality is provided by the Entity Translation (ET) module.
MAINTAINERS
-----------
* Jose Reyero - https://www.drupal.org/u/jose-reyero
* Florian Weber (webflo) - https://www.drupal.org/u/webflo
* Peter Philipp - https://www.drupal.org/u/das-peter
* Joseph Olstad - https://www.drupal.org/u/joseph.olstad
* Nathaniel Catchpole - https://www.drupal.org/u/catch
@@ -0,0 +1,53 @@
<?php
/**
* @file
* API documentation file for Field translation module.
*
* This module takes care of translating common field elements like title and
* description for all fields, plus some field specific values (default, options)
* for field types defined by Drupal core.
*
* Before implementing any of these hooks, consider whether you would be better
* off implementing Drupal core's hook_field_widget_form_alter().
*
* @see i18n_field_field_widget_form_alter()
*/
/**
* Provide information about callbacks for translating specific field types.
*
* This information can be retrieved using i18n_field_type_info().
* @return
* Array of values indexed by field type. Valid keys are:
* - 'translate_default', Callback for translating the default value for this field type.
* - 'translate_options', Callback for translating options for this field type.
*
* @see i18n_field_type_info()
* @see i18n_field_i18n_field_info()
*
* For examples of both callback types:
*
* @see i18n_field_translate_allowed_values()
* @see i18n_field_translate_default()
*
*/
function hook_i18n_field_info() {
$info['text'] = $info['text_long'] = $info['text_with_summary'] = array(
'translate_default' => 'i18n_field_translate_default',
);
$info['list_text'] = $info['list_boolean'] = $info['list_integer'] = array(
'translate_options' => 'i18n_field_translate_allowed_values',
);
return $info;
}
/**
* Alter information provided by hook_i18n_field_info().
*
* @see i18n_field_type_info()
*/
function hook_i18n_field_info_alter(&$info) {
// Unset the default callback for text fields.
unset($info['text']['translate_default']);
}
@@ -72,6 +72,7 @@ function i18n_field_i18n_string_info() {
'description' => t('Configurable fields descriptions, defaults, options, etc.'),
'format' => FALSE, // This group doesn't have formatted strings
'list' => TRUE, // This group can list all strings
'class' => variable_get('i18n_string_textgroup_class_field', 'i18n_string_textgroup_cached'),
);
return $groups;
}
@@ -6,9 +6,8 @@ package = Multilingual - Internationalization
core = 7.x
files[] = i18n_field.inc
files[] = i18n_field.test
; Information added by drupal.org packaging script on 2013-01-13
version = "7.x-1.8"
; Information added by Drupal.org packaging script on 2020-06-17
version = "7.x-1.27"
core = "7.x"
project = "i18n"
datestamp = "1358075001"
datestamp = "1592373390"
@@ -163,7 +163,6 @@ function i18n_field_field_formatter_view($entity_type, $entity, $field, $instanc
return $element;
}
/**
* Implements hook_field_widget_form_alter().
*
@@ -176,8 +175,8 @@ function i18n_field_field_formatter_view($entity_type, $entity, $field, $instanc
function i18n_field_field_widget_form_alter(&$element, &$form_state, $context) {
global $language;
// Skip the node type edit fields by checking for existing entity
if (empty($element['#entity'])) {
// Don't translate if the widget is being shown on the field edit form.
if ($form_state['build_info']['form_id'] == 'field_ui_field_edit_form') {
return;
}
@@ -189,8 +188,45 @@ function i18n_field_field_widget_form_alter(&$element, &$form_state, $context) {
$instance = $context['instance'];
$langcode = $context['langcode'];
// Get the element to alter. Account for inconsistencies in how the element
// is built for different field types.
if (isset($element[0]) && count($element) == 1) {
// Single-value file fields and image fields.
$alter_element = &$element[0];
}
elseif ($field['type'] == 'url' && $field['module'] == 'url' && $field['cardinality'] == 1) {
$alter_element = &$element;
}
elseif (isset($element['value'])) {
// Number fields. Single-value text fields.
$alter_element = &$element['value'];
}
elseif ($field['type'] == 'entityreference' && isset($element['target_id'])) {
// Entityreference fields using the entityreference_autocomplete widget.
$alter_element = &$element['target_id'];
}
elseif ($field['type'] == 'node_reference' && isset($element['nid'])) {
// The node_reference fields using the entityreference_autocomplete widget.
$alter_element = &$element['nid'];
}
else {
// All other fields.
$alter_element = &$element;
}
// If a subelement has the same title as the parent, translate it instead.
// Allows fields such as email and commerce_price to be translated.
foreach (element_get_visible_children($element) as $key) {
$single_value = ($field['cardinality'] == 1);
$has_title = (isset($element['#title']) && isset($element[$key]['#title']));
if ($single_value && $has_title && $element[$key]['#title'] == $element['#title']) {
$alter_element = &$element[$key];
break;
}
}
// The field language may affect some variables (default) but not others (description will be in current page language)
$i18n_langcode = empty($element['#language']) || $element['#language'] == LANGUAGE_NONE ? $language->language : $element['#language'];
$i18n_langcode = empty($alter_element['#language']) || $alter_element['#language'] == LANGUAGE_NONE ? $language->language : $alter_element['#language'];
// Translate instance to current page language and set to form_state
// so it will be used for validation messages later.
@@ -200,37 +236,60 @@ function i18n_field_field_widget_form_alter(&$element, &$form_state, $context) {
}
// Translate field title if set and it is the default one.
// When cardinality is 1, $element['value'] is used instead.
if (!empty($instance_current['label']) && $instance_current['label'] != $instance['label']) {
if (!empty($element['#title']) && $element['#title'] == $instance['label']) {
$element['#title'] = $instance_current['label'];
}
if (isset($element['value']) && !empty($element['value']['#title']) && $element['value']['#title'] == $instance['label']) {
$element['value']['#title'] = $instance_current['label'];
if (!empty($alter_element['#title']) && $alter_element['#title'] == check_plain($instance['label'])) {
$alter_element['#title'] = check_plain($instance_current['label']);
}
}
// Translate field description if set and it is the default one.
// When cardinality is 1, $element['value'] is used instead.
if (!empty($instance_current['description']) && $instance_current['description'] != $instance['description']) {
if (!empty($element['#description']) && $element['#description'] == $instance['description']) {
$element['#description'] = $instance_current['description'];
}
if (isset($element['value']) && !empty($element['value']['#description']) && $element['value']['#description'] == $instance['description']) {
$element['value']['#description'] = $instance_current['description'];
if (!empty($alter_element['#description'])) {
// Allow single-value file fields and image fields to have their
// descriptions translated. file_field_widget_form() passes the
// description through theme('file_upload_help'), so i18n_field
// must do the same.
$filefield = in_array($field['type'], array('file', 'image'));
$single_value = ($field['cardinality'] == 1);
$no_default = empty($alter_element['#default_value']['fid']);
if ($filefield && $single_value && $no_default) {
$help_variables = array(
'description' => field_filter_xss($instance['description']),
'upload_validators' => isset($alter_element['#upload_validators']) ? $alter_element['#upload_validators'] : array(),
);
$original_description = theme('file_upload_help', $help_variables);
if ($alter_element['#description'] == $original_description) {
$help_variables = array(
'description' => field_filter_xss($instance_current['description']),
'upload_validators' => isset($alter_element['#upload_validators']) ? $alter_element['#upload_validators'] : array(),
);
$alter_element['#description'] = theme('file_upload_help', $help_variables);
}
}
elseif ($alter_element['#description'] == field_filter_xss($instance['description'])) {
$alter_element['#description'] = field_filter_xss($instance_current['description']);
}
}
}
// Translate list options
if (!empty($element['#options']) && ($translate = i18n_field_type_info($field['type'], 'translate_options')) && !empty($field['settings']['allowed_values'])) {
$element['#options'] = $translate($field, $i18n_langcode);
if (isset($element['#properties']) && !empty($element['#properties']['empty_option'])) {
$label = theme('options_none', array('instance' => $instance, 'option' => $element['#properties']['empty_option']));
$element['#options'] = array('_none' => $label) + $element['#options'];
// For some elements, change title to new translated option
if (!empty($element['#title']) && $field['type'] == 'list_boolean' && !empty($element['#on_value'])) {
$on_value = $element['#on_value'];
$element['#title'] = $element['#options'][$on_value];
// Translate list options.
$has_options = (!empty($alter_element['#options']) || $field['type'] == 'list_boolean');
$has_allowed_values = !empty($field['settings']['allowed_values']);
$translate = i18n_field_type_info($field['type'], 'translate_options');
if ($has_options && $has_allowed_values && $translate) {
$alter_element['#options'] = $translate($field, $i18n_langcode);
if (isset($alter_element['#properties']) && !empty($alter_element['#properties']['empty_option'])) {
$label = theme('options_none', array('instance' => $instance, 'option' => $alter_element['#properties']['empty_option']));
$alter_element['#options'] = array('_none' => $label) + $alter_element['#options'];
}
// Translate list_boolean fields using the checkboxes widget.
if (!empty($alter_element['#title']) && $field['type'] == 'list_boolean' && !empty($alter_element['#on_value'])) {
$on_value = $alter_element['#on_value'];
$alter_element['#options'];
$alter_element['#title'] = $alter_element['#options'][$on_value];
// For using label instead of "On value".
if ($instance['widget']['settings']['display_label']) {
$alter_element['#title'] = $instance_current['label'];
}
}
}
@@ -242,11 +301,15 @@ function i18n_field_field_widget_form_alter(&$element, &$form_state, $context) {
$delta = $context['delta'];
$items = $context['items'];
// Translate default value if exists and the current value is the default
if (isset($element['value']['#default_value']) && ($translate = i18n_field_type_info($field['type'], 'translate_default')) &&
!empty($instance['default_value'][$delta]['value']) && !empty($items[$delta]['value']) &&
$instance['default_value'][$delta]['value'] === $items[$delta]['value']) {
$element['value']['#default_value'] = $translate($instance, $items[$delta]['value'], $i18n_langcode);
// Translate default value.
$has_default_value = (isset($alter_element['#default_value']) && !empty($instance['default_value'][$delta]['value']));
$storage_has_value = !empty($items[$delta]['value']);
$translate = i18n_field_type_info($field['type'], 'translate_default');
if ($has_default_value && $storage_has_value && $translate) {
// Compare the default value with the value currently in storage.
if ($instance['default_value'][$delta]['value'] === $items[$delta]['value']) {
$alter_element['#default_value'] = $translate($instance, $items[$delta]['value'], $i18n_langcode);
}
}
}
@@ -344,10 +407,12 @@ function i18n_field_translate_allowed_values($field, $langcode = NULL) {
}
/**
* Translate field default
* Translate field default.
*/
function i18n_field_translate_default($instance, $value, $langcode = NULL) {
return i18n_string_translate(array('field', $instance['field_name'], $instance['bundle'], 'default_value'), $value, array('langcode' => $langcode));
// The default value does not need sanitizing in a text_textfield widget.
$sanitize = !($instance['widget']['type'] == 'text_textfield' && $instance['widget']['module'] == 'text');
return i18n_string_translate(array('field', $instance['field_name'], $instance['bundle'], 'default_value'), $value, array('langcode' => $langcode, 'sanitize' => $sanitize));
}
/**
@@ -360,7 +425,17 @@ function i18n_field_translate_property($instance, $property, $langcode = NULL) {
}
/**
* Get i18n information for fields
* Get i18n information for translating fields.
*
* @param $type
* Optional field type.
* @param $property
* Optional property to get from field type.
*
* @return
* - The property for the field if $type and $property set.
* - Array of properties for the field type if only $type is set.
* - Array of translation information for all field types.
*/
function i18n_field_type_info($type = NULL, $property = NULL) {
$info = &drupal_static(__FUNCTION__);
@@ -378,3 +453,120 @@ function i18n_field_type_info($type = NULL, $property = NULL) {
return $info;
}
}
/**
* Implements hook_field_info_alter().
*/
function i18n_field_field_info_alter(&$field_info) {
foreach(array_keys($field_info) as $type) {
$field_info[$type]['property_callbacks'][] = 'i18n_field_entity_property_callback';
}
}
/**
* Prime the cache to avoid single db queries for entity fields / properties.
*
* This is mainly uses when large operations are occuring like a flush of the
* entity_property_infos().
*/
function i18n_field_prime_caches() {
global $language;
static $cache_primed;
// Fill the cache. This should avoid single db queries when filling the
// properties.
if (empty($cache_primed)) {
$cache_primed = TRUE;
$text_group = i18n_string_textgroup('field');
// Load all strings at once to avoid callbacks for each individual string.
$text_group->load_strings();
$text_group->multiple_translation_search(array('type' => '*', 'objectid' => '*', 'property' => '*'), $language->language);
}
}
/**
* Callback to translate entity property info for a fields.
*
* @see entity_metadata_field_entity_property_info()
* @see entity_metadata_field_default_property_callback()
* @see i18n_field_i18n_object_info_alter()
* @see hook_module_implements_alter()
*/
function i18n_field_entity_property_callback(&$info, $entity_type, $field, $instance, $field_type) {
global $language;
// This could create a endless recursion if it's called during rebuilding the
// cache for i18n_object_info(). So if the cache of i18n_object_info isn't
// available yet we assume the worst case, leave the info alone but trigger a
// rebuild of the property when hook_i18n_object_info_alter is invoked. At
// that point the info is available and we can rely on it.
if (!$info = &drupal_static('i18n_object_info')) {
$i18n_field_entity_property_callback_fallback = &drupal_static(__FUNCTION__);
$i18n_field_entity_property_callback_fallback = TRUE;
return;
}
i18n_field_prime_caches();
$name = $field['field_name'];
$property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$name];
$property['label'] = i18n_field_translate_property($instance, 'label', $language->language);
}
/**
* Implements hook_i18n_object_info_alter().
*/
function i18n_field_i18n_object_info_alter(&$info) {
if (drupal_static('i18n_field_entity_property_callback')) {
if ($info = drupal_static('i18n_object_info')) {
// Clean static and permanent cache of the data and then re-run the property
// building.
// Use a lock to avoid stampeding.
$lock_name = 'i18n_field_entity_property_callback_fallback:' . $GLOBALS['language']->language;
// See if another request is already doing this. If so we bail out here as
// we won't help with anything at the moment.
if (!lock_may_be_available($lock_name)) {
return;
}
if (lock_acquire($lock_name)) {
i18n_field_prime_caches();
// Inject translated properties.
$entity_property_info = entity_get_property_info();
foreach ($entity_property_info as $entity_type => $properties) {
if (isset($properties['bundles'])) {
foreach ($properties['bundles'] as $bundle => $bundle_properties) {
if ($bundle_properties['properties']) {
foreach ($bundle_properties['properties'] as $bundle_property => $bundle_property_info) {
if ($instance = field_info_instance($entity_type, $bundle_property, $bundle)) {
$property = &$entity_property_info[$entity_type]['bundles'][$instance['bundle']]['properties'][$bundle_property];
$property['label'] = i18n_field_translate_property($instance, 'label', $GLOBALS['language']->language);
}
}
}
}
}
}
// Inject into static cache.
$entity_get_property_info = &drupal_static('entity_get_property_info', array());
$entity_get_property_info = $entity_property_info;
// Write permanent cache.
cache_set('entity_property_info:' . $GLOBALS['language']->language, $entity_property_info);
lock_release($lock_name);
}
}
else {
watchdog('i18n_field', 'Unable to run fall-back handling for entity property translation due missing "i18n_object_info" cache', array(), WATCHDOG_WARNING);
}
}
}
/**
* Implements hook_module_implements_alter().
*/
function i18n_field_module_implements_alter(&$implementations, $hook) {
if ($hook == 'i18n_object_info_alter') {
// Move our hook implementation to the bottom.
$group = $implementations['i18n_field'];
unset($implementations['i18n_field']);
$implementations['i18n_field'] = $group;
}
}
@@ -0,0 +1,86 @@
CONTENTS OF THIS FILE
---------------------
* Introduction
* Requirements
* Recommended modules
* Installation
* Configuration
* Maintainers
INTRODUCTION
------------
The Multilingual forum module, part of the Internationalization
(https://www.drupal.org/project/i18n) package, helps with the multilingual
configuration of the sites forums.
* For a full description of the module visit
https://www.drupal.org/node/1396988.
* To submit bug reports and feature suggestions, or to track changes visit
https://www.drupal.org/project/issues/i18n.
REQUIREMENTS
------------
This module requires the following module:
* Internationalization - https://www.drupal.org/project/i18n
RECOMMENDED MODULES
-------------------
* Internationalization Views - https://www.drupal.org/project/i18nviews
* Language Icons - https://www.drupal.org/project/languageicons
* Translation Overview - https://www.drupal.org/project/translation_overview
* Localization Client - https://www.drupal.org/project/l10n_client
* Internationalization contributions -
https://www.drupal.org/project/i18n_contrib
INSTALLATION
------------
This is a submodule of the Internationalization module. Install the
Internationalization module as you would normally install a contributed Drupal
module. Visit https://www.drupal.org/node/895232 for further information.
CONFIGURATION
-------------
To configure forum vocabulary
1. Enable the Multilingual forum module included with Internationalization.
2. Go to Administration > Structure > Taxonomy.
3. Select the "edit vocabulary" for the Forums vocabulary.
4. Choose the translation mode (Localize or Translate).
5. Select the "Save and translate" button.
6. Select the "translate" link for a language.
7. Translate the "Name" and "Description" for the forum.
8. Select the "Save translation" button.
9. Repeat steps 6 to 8 for each language.
To configure forum terms
1. Go to Administration > Structure > Forums.
2. Select the "edit" link for a forum or container.
3. Select the "Translate" tab.
4. Select the "translate" link for a language.
5. Translate the "Name" and "Description" for the term.
6. Select the "Save translation" button.
7. Repeat steps 4 to 6 for each language.
8. Repeat all steps for all terms.
MAINTAINERS
-----------
* Jose Reyero - https://www.drupal.org/u/jose-reyero
* Florian Weber (webflo) - https://www.drupal.org/u/webflo
* Peter Philipp - https://www.drupal.org/u/das-peter
* Joseph Olstad - https://www.drupal.org/u/joseph.olstad
* Nathaniel Catchpole - https://www.drupal.org/u/catch
@@ -7,9 +7,8 @@ package = Multilingual - Internationalization
core = 7.x
files[] = i18n_forum.test
; Information added by drupal.org packaging script on 2013-01-13
version = "7.x-1.8"
; Information added by Drupal.org packaging script on 2020-06-17
version = "7.x-1.27"
core = "7.x"
project = "i18n"
datestamp = "1358075001"
datestamp = "1592373390"
@@ -35,7 +35,7 @@ function i18n_forum_menu_local_tasks_alter(&$data, $router_item, $root_path) {
if ($vid && ($vocabulary = taxonomy_vocabulary_load($vid)) && ($field = field_info_field('taxonomy_' . $vocabulary->machine_name))) {
foreach ($field['bundles']['node'] as $type) {
if (isset($data['actions']['output'][$type])) {
$data['actions']['output'][$type]['#link']['title'] = t('Add new @node_type', array('@node_type' => i18n_node_type_name($type, node_type_get_name($type))));
$data['actions']['output'][$type]['#link']['title'] = t('Add new @node_type', array('@node_type' => i18n_node_type_name($type)));
}
}
}
@@ -0,0 +1,111 @@
CONTENTS OF THIS FILE
---------------------
* Introduction
* Requirements
* Recommended modules
* Installation
* Configuration
* Troubleshooting
* Maintainers
INTRODUCTION
------------
The Menu translation module, part of the Internationalization
(https://www.drupal.org/project/i18n) package, allows users to select a
translation mode for each menu.
* For a full description of the module, visit this page:
https://www.drupal.org/node/1113982.
* To submit bug reports and feature suggestions, or to track changes:
https://www.drupal.org/project/issues/i18n.
REQUIREMENTS
------------
This module requires the following module:
* Internationalization - https://www.drupal.org/project/i18n
RECOMMENDED MODULES
-------------------
* Internationalization Views - https://www.drupal.org/project/i18nviews
* Language Icons - https://www.drupal.org/project/languageicons
* Translation Overview - https://www.drupal.org/project/translation_overview
* Localization Client - https://www.drupal.org/project/l10n_client
* Internationalization contributions -
https://www.drupal.org/project/i18n_contrib
To link menu item menus to nodes, it is useful to have the following modules:
* Entity translation i18n menu module, a submodule of Entity translation -
https://www.drupal.org/project/entity_translation
* Menu translation node module - https://www.drupal.org/project/i18n_menu_node
INSTALLATION
------------
This is a submodule of the Internationalization module. Install the
Internationalization module as you would normally install a contributed Drupal
module. Visit https://www.drupal.org/node/895232 for further information.
CONFIGURATION
-------------
Language-specific menus
1. To create or edit a menu, navigate to Structure > Menus > (menu to edit) >
Edit.
2. In the Translation mode section, choose Fixed Language and a Language field
will appear.
3. Select a language, select Save, and add or update the menu items as needed.
The menu block will only appear when viewing content in the same language.
There are three modes available: Translate and Localize, Fixed Language, and No
Multilingual Options.
Translate and Localize:
The user can create one menu for all languages, and translate or localize each
menu item. There are two ways that menu items will be translated.
1. The user can set a language when creating a custom menu item so that the menu
item will only show up for that language. Menu items that link to nodes in a
particular language will be treated this way.
2. The user can localize other custom menu items without a language
(for example, menu items linking to Views pages). Use the Translate tab to
translate the menu item title and description. Translators can also use the
"Translate interface" pages to translate these menu items.
Fixed Language:
If the user chooses Fixed Language, they'll have to set up a separate menu in
each language. This could become tedious if have a lot of languages enabled on
the site, but is useful if the content or menu structure is different for each
language.
No Multilingual Options:
Only the menu will be translatable.
TROUBLESHOOTING
---------------
A menu item linked to a node will be displayed only when the node language
matches the page language. This is due to how the menu system works and the
"Language selection" feature in i18n. Therefore, to get translated menus items
that link to nodes, you first need translated content. For more information
visit https://www.drupal.org/docs/7/multilingual/translating-content.
MAINTAINERS
-----------
* Jose Reyero - https://www.drupal.org/u/jose-reyero
* Florian Weber (webflo) - https://www.drupal.org/u/webflo
* Peter Philipp - https://www.drupal.org/u/das-peter
* Joseph Olstad - https://www.drupal.org/u/joseph.olstad
* Nathaniel Catchpole - https://www.drupal.org/u/catch
@@ -10,7 +10,7 @@
* Produces a menu translation form.
*/
function i18n_menu_translation_form($form, $form_state, $translation_set = NULL, $item = NULL) {
$translation_set = $translation_set ? $translation_set : i18n_translation_set_create('menu_link');
$translation_set = $translation_set ? $translation_set : i18n_translation_set_build('menu_link');
$form['translation_set'] = array('#type' => 'value', '#value' => $translation_set);
$translations = $translation_set->get_translations();
// What to do with title? Let's make it a hidden field for now, some tests relay on it
@@ -55,7 +55,7 @@ function i18n_menu_translation_form($form, $form_state, $translation_set = NULL,
}
$form['actions'] = array('#type' => 'actions');
$form['actions']['update'] = array('#type' => 'submit', '#value' => t('Save'));
if ($translation_set) {
if (!empty($translation_set->tsid)) {
$form['actions']['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
}
return $form;
@@ -244,12 +244,16 @@ function i18n_menu_translation_item_overview($item, $translation_set = NULL) {
$title = t('n/a');
$options[] = l(t('add translation'), 'admin/structure/menu/manage/' . $item['menu_name'] . '/add', array('query' => array('translation' => $item['mlid'], 'target' => $langcode) + drupal_get_destination()));
}
$rows[] = array($language_name, $title, implode(" | ", $options));
$rows[$langcode] = array(
'language' => $language_name,
'title' => $title,
'operations' => implode(" | ", $options)
);
}
drupal_set_title(t('Translations of menu item %title', array('%title' => $item['link_title'])), PASS_THROUGH);
$build['translation_item_overview'] = array(
$build['translation_overview'] = array(
'#theme' => 'table',
'#header' => $header,
'#rows' => $rows,
@@ -123,8 +123,8 @@ function i18n_menu_item_translation_page($type, $item) {
module_load_include('admin.inc', 'i18n_menu');
// If the item has a language code, we can only support translation sets.
$translation_set = !empty($item['i18n_tsid']) ? i18n_translation_set_load($item['i18n_tsid']) : NULL;
$build['overview'] = i18n_menu_translation_item_overview($item, $translation_set);
$build['translation_form'] = drupal_get_form('i18n_menu_translation_form', $translation_set, $item);
return $build;
$overview = i18n_menu_translation_item_overview($item, $translation_set);
$translation_form = drupal_get_form('i18n_menu_translation_form', $translation_set, $item);
return $overview + $translation_form;
}
@@ -69,6 +69,7 @@ class i18n_menu_link extends i18n_string_object_wrapper {
return I18N_MODE_NONE;
}
}
/**
* Access to object translation. This should check object properties and permissions
*/
@@ -10,9 +10,8 @@ core = 7.x
files[] = i18n_menu.inc
files[] = i18n_menu.test
; Information added by drupal.org packaging script on 2013-01-13
version = "7.x-1.8"
; Information added by Drupal.org packaging script on 2020-06-17
version = "7.x-1.27"
core = "7.x"
project = "i18n"
datestamp = "1358075001"
datestamp = "1592373390"
@@ -53,6 +53,24 @@ function i18n_menu_menu_alter(&$items) {
$items['admin/structure/menu/item/%menu_link'] = $items['admin/structure/menu/item/%menu_link/edit'];
$items['admin/structure/menu/item/%menu_link']['type'] = MENU_CALLBACK;
$items['admin/structure/menu/item/%menu_link/edit']['type'] = MENU_DEFAULT_LOCAL_TASK;
$items['admin/structure/menu/manage/%menu']['title callback'] = 'i18n_menu_menu_overview_title';
}
/**
* Preprocess theme_menu_admin_overview to translate menu name and description
*
* @param $variables
*/
function i18n_menu_preprocess_menu_admin_overview(&$variables) {
$variables['title'] = i18n_string(array('menu', 'menu', $variables['name'], 'title'), $variables['title'], array('sanitize' => FALSE));
$variables['description'] = i18n_string(array('menu', 'menu', $variables['name'], 'description'), $variables['description'], array('sanitize' => FALSE));
}
/**
* Title callback for the menu overview page and links.
*/
function i18n_menu_menu_overview_title($menu) {
return i18n_string(array('menu', 'menu', $menu['menu_name'], 'title'), $menu['title']);
}
/**
@@ -343,6 +361,8 @@ function i18n_menu_variable_info_alter(&$variables, $options) {
// Make menu variables translatable
$variables['menu_main_links_source']['localize'] = TRUE;
$variables['menu_secondary_links_source']['localize'] = TRUE;
$variables['menu_parent_[node_type]']['localize'] = TRUE;
$variables['menu_options_[node_type]']['localize'] = TRUE;
}
/**
@@ -377,7 +397,7 @@ function i18n_menu_localize_tree($tree, $langcode = NULL) {
if (_i18n_menu_link_process($item['link'])) {
if (!_i18n_menu_link_is_visible($item['link'], $langcode)) {
// Remove links for other languages than current.
// Links with language wont be localized.
// Links with language won't be localized.
unset($tree[$index]);
// @todo Research whether the above has any advantage over:
// $item['hidden'] = TRUE;
@@ -468,7 +488,8 @@ function i18n_menu_navigation_links($menu_name, $level = 0) {
* Get localized menu title
*/
function _i18n_menu_link_title($link, $langcode = NULL) {
return i18n_string_translate(array('menu', 'item', $link['mlid'], 'title'), $link['link_title'], array('langcode' => $langcode, 'sanitize' => FALSE));
$key = i18n_object_info('menu_link', 'key');
return i18n_string_translate(array('menu', 'item', $link[$key], 'title'), $link['link_title'], array('langcode' => $langcode, 'sanitize' => FALSE));
}
/**
@@ -500,7 +521,8 @@ function _i18n_menu_link_localize(&$link, $langcode = NULL) {
*/
function _i18n_menu_link_description($link, $langcode = NULL) {
if (!empty($link['options']['attributes']['title'])) {
return i18n_string_translate(array('menu', 'item', $link['mlid'], 'description'), $link['options']['attributes']['title'], array('langcode' => $langcode));
$key = i18n_object_info('menu_link', 'key');
return i18n_string_translate(array('menu', 'item', $link[$key], 'description'), $link['options']['attributes']['title'], array('langcode' => $langcode, 'sanitize' => FALSE));
}
else {
return NULL;
@@ -511,9 +533,11 @@ function _i18n_menu_link_description($link, $langcode = NULL) {
* Check whether this link is to be processed by i18n_menu and start processing.
*/
function _i18n_menu_link_process(&$link) {
// Only visible links that have a language property and haven't been processed
// before. We also check that they belong to a menu with language options.
if (empty($link['i18n_menu']) && !empty($link['language']) && !empty($link['access']) && empty($link['hidden']) && i18n_menu_mode($link['menu_name'])) {
// Only links that have a language property and haven't been processed before.
// We also translate links marked as hidden because core breadcrumbs ignore
// that flag and excluding them would basically interfere with core behaviour.
// We also check that they belong to a menu with language options.
if (empty($link['i18n_menu']) && !empty($link['language']) && !empty($link['access']) && i18n_menu_mode($link['menu_name'])) {
// Mark so it won't be processed twice.
$link['i18n_menu'] = TRUE;
// Skip if administering this menu or this menu item.
@@ -525,6 +549,10 @@ function _i18n_menu_link_process(&$link) {
return FALSE;
}
}
// Skip if administering this menu item through the node edit form.
elseif (arg(0) == 'node' && arg(2) == 'edit' && $link['link_path'] == arg(0) . '/' . arg(1)) {
return FALSE;
}
return TRUE;
}
else {
@@ -571,7 +599,7 @@ function _i18n_menu_link_is_visible($link, $langcode = NULL) {
}
/**
* Get localizable properties for menu link checking agains the router item.
* Get localizable properties for menu link checking against the router item.
*/
function _i18n_menu_link_localizable_properties($link) {
$props = array();
@@ -580,7 +608,7 @@ function _i18n_menu_link_localizable_properties($link) {
// If the title callback is 't' and the link title matches the router title
// it will be localized by core, not by i18n_menu.
if (!$router ||
(empty($router['title_callback']) || $router['title_callback'] != 't') ||
(empty($router['title_callback']) || ($router['title_callback'] != 't' || !empty($link['customized']))) ||
(empty($router['title']) || $router['title'] != $link['link_title'])
) {
$props[] = 'title';
@@ -589,7 +617,7 @@ function _i18n_menu_link_localizable_properties($link) {
if (!empty($link['options']['attributes']['title'])) {
// If the description matches the router description, it will be localized
// by core.
if (!$router || empty($router['description']) || $router['description'] != $link['options']['attributes']['title']) {
if (!$router || empty($router['description']) || ($router['description'] != $link['options']['attributes']['title']) || !empty($link['customized'])) {
$props[] = 'description';
}
}
@@ -709,6 +737,30 @@ function i18n_menu_form_menu_edit_item_alter(&$form, &$form_state) {
array_unshift($form['#validate'], 'i18n_menu_menu_item_prepare_normal_path');
}
/**
* Implements hook_form_FORM_ID_alter().
* FORM_ID = menu-overview-form.
* Add a "translate" link in operations column for each menu item.
*/
function i18n_menu_form_menu_overview_form_alter(&$form, &$form_state) {
if (i18n_menu_mode($form['#menu']['menu_name'], I18N_MODE_MULTIPLE)) {
foreach (element_children($form) as $element) {
if (substr($element, 0, 5) == 'mlid:') {
$item = $form[$element]["#item"];
$mlid = $form[$element]['#item']['mlid'];
if (i18n_get_object('menu', $mlid)->get_translate_access()) {
$form[$element]['operations']['translate'] = array(
'#type' => 'link',
'#title' => t('translate'),
'#href' => "admin/structure/menu/item/{$mlid}/translate",
);
$form[$element]['title']['#markup'] = l(_i18n_menu_link_title($item), $item['href'], $item['localized_options']);
}
}
}
}
}
/**
* Normal path should be checked with menu item's language to avoid
* troubles when a node and it's translation has the same url alias.
@@ -727,6 +779,9 @@ function i18n_menu_item_get_language($item) {
}
else {
$menu = menu_load($item['menu_name']);
if (!isset($menu['i18n_mode'])) {
return LANGUAGE_NONE;
}
switch ($menu['i18n_mode']) {
case I18N_MODE_LANGUAGE:
return $menu['language'];
@@ -859,93 +914,33 @@ function i18n_menu_link_load($path, $langcode) {
}
/**
* Implements hook_init().
* Implements hook_query_TAG_alter() for features_menu_links.
* Add needed fields to properly serialize localization information.
*/
function i18n_menu_init() {
function i18n_menu_query_features_menu_link_alter($query) {
$query->fields('menu_links', array('language', 'customized'));
}
// The only way to override the default preferred menu link for a path is to
// inject it into the static cache of the function menu_link_get_preferred().
/**
* Implements hook_query_TAG_alter()
*
* Using tag 'preferred_menu_links' added in menu_link_get_preferred().
* See http://drupal.org/node/1854134
*/
function i18n_menu_query_preferred_menu_links_alter(QueryAlterableInterface $query) {
global $language;
// Get queried tables.
$tables = $query->getTables();
// The problem with the default implementation is that it does not take the
// language of a menu link into account. Whe having different menu trees for
// different menus, this means that the active trail will not work for all but
// one language.
// The code below is identical to the mentioned function except the added
// language condition on the query.
// TODO: Adding an alter tag to the query would allow to do this with a simple
// hook_query_alter() implementation.
$preferred_links = &drupal_static('menu_link_get_preferred');
$path = $_GET['q'];
// Look for the correct menu link by building a list of candidate paths,
// which are ordered by priority (translated hrefs are preferred over
// untranslated paths). Afterwards, the most relevant path is picked from
// the menus, ordered by menu preference.
$item = menu_get_item($path);
$path_candidates = array();
// 1. The current item href.
$path_candidates[$item['href']] = $item['href'];
// 2. The tab root href of the current item (if any).
if ($item['tab_parent'] && ($tab_root = menu_get_item($item['tab_root_href']))) {
$path_candidates[$tab_root['href']] = $tab_root['href'];
}
// 3. The current item path (with wildcards).
$path_candidates[$item['path']] = $item['path'];
// 4. The tab root path of the current item (if any).
if (!empty($tab_root)) {
$path_candidates[$tab_root['path']] = $tab_root['path'];
}
// Retrieve a list of menu names, ordered by preference.
$menu_names = menu_get_active_menu_names();
// Use an illegal menu name as the key for the preferred menu link.
$selected_menu = MENU_PREFERRED_LINK;
// Put the selected menu at the front of the list.
array_unshift($menu_names, $selected_menu);
$query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC));
$query->leftJoin('menu_router', 'm', 'm.path = ml.router_path');
$query->fields('ml');
// Weight must be taken from {menu_links}, not {menu_router}.
$query->addField('ml', 'weight', 'link_weight');
$query->fields('m');
$query->condition('ml.link_path', $path_candidates, 'IN');
// Only look menu links with none or the current language.
$query->condition('ml.language', array(LANGUAGE_NONE, i18n_language_interface()->language), 'IN');
// Sort candidates by link path and menu name.
$candidates = array();
foreach ($query->execute() as $candidate) {
$candidate['weight'] = $candidate['link_weight'];
$candidates[$candidate['link_path']][$candidate['menu_name']] = $candidate;
// Add any menus not already in the menu name search list.
if (!in_array($candidate['menu_name'], $menu_names)) {
$menu_names[] = $candidate['menu_name'];
}
}
// Store the most specific link for each menu. Also save the most specific
// link of the most preferred menu in $preferred_link.
foreach ($path_candidates as $link_path) {
if (isset($candidates[$link_path])) {
foreach ($menu_names as $menu_name) {
if (empty($preferred_links[$path][$menu_name]) && isset($candidates[$link_path][$menu_name])) {
$candidate_item = $candidates[$link_path][$menu_name];
$map = explode('/', $path);
_menu_translate($candidate_item, $map);
if ($candidate_item['access']) {
$preferred_links[$path][$menu_name] = $candidate_item;
if (empty($preferred_links[$path][MENU_PREFERRED_LINK])) {
// Store the most specific link.
$preferred_links[$path][MENU_PREFERRED_LINK] = $candidate_item;
}
}
}
foreach ($tables as $alias => $table) {
if ($table['table'] == 'menu_links') {
// Add language filter, ensuring that we don't have any collision when
// determining the active menu trail when there are multiple menu items
// with same link path but different languages.
if ($language) {
$query->condition('language', array($language->language, LANGUAGE_NONE), 'IN');
}
break;
}
}
}
@@ -130,6 +130,8 @@ class i18nMenuTestCase extends Drupali18nTestCase {
$this->drupalGet('admin/structure/menu/item/' . $node->menu['mlid'] . '/edit');
$this->assertText(t('This menu item belongs to a node, so it will have the same language as the node and cannot be localized.'));
$this->assertNoField('language', 'We cannot edit language for menu items that belong to nodes.');
// Edit the node and submit it to see if the menu link stays enabled.
$this->drupalPost('node/' . $node->nid . '/edit', array(), t('Save'));
}
// Check menu items show up for the right language.
$this->drupalGet('<front>');
@@ -0,0 +1,96 @@
CONTENTS OF THIS FILE
---------------------
* Introduction
* Requirements
* Recommended modules
* Installation
* Configuration
* Maintainers
INTRODUCTION
------------
The Multilingual content module, part of the Internationalization
(https://www.drupal.org/project/i18n) package, provides extended multilingual
options for nodes. These options help accommodate a variety of translation
workflows by controlling how the language for nodes is set.
Note that the Multilingual content module lives in the i18n_node directory
in the Internationalization package. Don't confuse this module with the core
Content translation module.
* For a full description of the module visit:
https://www.drupal.org/node/1279644.
* To submit bug reports and feature suggestions, or to track changes visit
https://www.drupal.org/project/issues/i18n.
REQUIREMENTS
------------
This module requires the following module:
* Internationalization - https://www.drupal.org/project/i18n
RECOMMENDED MODULES
-------------------
* Internationalization Views - https://www.drupal.org/project/i18nviews
* Language Icons - https://www.drupal.org/project/languageicons
* Translation Overview - https://www.drupal.org/project/translation_overview
* Localization Client - https://www.drupal.org/project/l10n_client
* Internationalization contributions -
https://www.drupal.org/project/i18n_contrib
INSTALLATION
------------
This is a submodule of the Internationalization module. Install the
Internationalization module as you would normally install a contributed Drupal
module. Visit https://www.drupal.org/node/895232 for further information.
CONFIGURATION
-------------
For each content type, the following Extended language options are available
under the Multilingual Settings tab.
1. "Set current language as default for new content" can be useful for content
that is community-generated.
2. "Require language" prevents users from creating 'Language neutral' nodes.
3. "Lock language" prevents users from changing the language of a node after
it's created.
Site-wide Settings for Node Translation
1. There are also site-wide settings provided to help streamline how
multilingual content is created. Navigate to Config > Regional and language >
Multilingual settings > Node Options.
2. "Switch interface for translating" switches the language of the user
interface to the chosen language when a user translates a node. This is
useful if users speak the language in which the translation is written. It
means that after the node translation is saved, the language of the UI will
match the language of the node.
3. "Hide content translation links" will prevent the language switcher links
from appearing in nodes and teasers. This is useful if a language switcher
block is enabled on the site.
4. The user can also select the default language for new nodes if the
corresponding content type doesn't have language support. By default, it is
set to be in the default language of the site, but this can be changed to be
language neutral. This is a useful option when thinking about forward
compatibility for adding multilingual support to these content types in the
future.
MAINTAINERS
-----------
* Jose Reyero - https://www.drupal.org/u/jose-reyero
* Florian Weber (webflo) - https://www.drupal.org/u/webflo
* Peter Philipp - https://www.drupal.org/u/das-peter
* Joseph Olstad - https://www.drupal.org/u/joseph.olstad
* Nathaniel Catchpole - https://www.drupal.org/u/catch
@@ -9,9 +9,8 @@ configure = admin/config/regional/i18n/node
files[]=i18n_node.test
files[]=i18n_node.variable.inc
; Information added by drupal.org packaging script on 2013-01-13
version = "7.x-1.8"
; Information added by Drupal.org packaging script on 2020-06-17
version = "7.x-1.27"
core = "7.x"
project = "i18n"
datestamp = "1358075001"
datestamp = "1592373390"
@@ -54,7 +54,7 @@ function i18n_node_block_view_system_help_alter(&$block) {
$arg = drupal_help_arg(arg(NULL));
if ($arg[0] == 'node' && $arg[1] == 'add' && $arg[2]) {
if (($type = node_type_get_type(str_replace('-', '_', $arg[2]))) && !empty($type->help)) {
$help = i18n_node_translate_type($type, 'help');
$help = i18n_node_translate_type($type, 'help', NULL, array('sanitize' => FALSE));
if ($help !== $type->help) {
$block['content'] = str_replace(filter_xss_admin($type->help), filter_xss_admin($help), $block['content']);
}
@@ -64,7 +64,7 @@ function i18n_node_block_view_system_help_alter(&$block) {
$node = menu_get_object();
if ($node && isset($node->type)) {
$type = node_type_get_type($node->type);
$help = i18n_node_translate_type($type, 'help');
$help = i18n_node_translate_type($type, 'help', NULL, array('sanitize' => FALSE));
if ($help !== $type->help) {
$block['content'] = str_replace(filter_xss_admin($type->help), filter_xss_admin($help), $block['content']);
}
@@ -121,6 +121,8 @@ function i18n_node_i18n_translate_path($path) {
'href' => 'node/' . $node_translation->nid . $matches[2],
'title' => $node_translation->title,
'object' => $node_translation,
// Performance: for node view add access information right away.
'access' => !$matches[2] ? $node_translation->status : NULL,
);
}
return $result;
@@ -220,8 +222,16 @@ function i18n_node_language_mode($type) {
function i18n_node_node_prepare($node) {
$options = variable_get('i18n_node_options_' . $node->type, array());
if (i18n_node_type_enabled($node) && empty($node->nid) && !i18n_object_langcode($node) && in_array('current', $options)) {
$default = variable_get('i18n_node_default_language_for_' . $node->type, '-- current --');
// Set current language for new nodes if option enabled
$node->language = i18n_language_content()->language;
if ($default === '-- current --') {
$node->language = i18n_language_content()->language;
}
// If a custom language was specified, apply it.
else {
$node->language = $default;
}
}
}
@@ -247,13 +257,16 @@ function i18n_node_permission() {
/**
* Implements hook_node_view()
*/
function i18n_node_node_view($node) {
function i18n_node_node_view($node, $view_mode, $langcode) {
if (i18n_node_type_enabled($node)) {
$node->content['language'] = array(
'#type' => 'item',
'#title' => t('Language'),
'#markup' => i18n_language_name($node->language),
);
$extra_fields_display_settings = field_extra_fields_get_display('node', $node->type, $view_mode);
if ($extra_fields_display_settings['language']['visible']) {
$node->content['language'] = array(
'#type' => 'item',
'#title' => t('Language'),
'#markup' => i18n_language_name($node->language),
);
}
}
}
@@ -263,7 +276,9 @@ function i18n_node_node_view($node) {
* Handles links for extended languages. Sets current interface language.
*/
function i18n_node_node_view_alter(&$build) {
$node = $build['#node'];
if (isset($build['#node'])) {
$node = $build['#node'];
}
// Hide node translation links.
if (variable_get('i18n_hide_translation_links', 0)) {
if (isset($build['links']['translation'])) {
@@ -347,7 +362,7 @@ function i18n_node_textfield_process($element) {
static $sent = FALSE;
// Ensure we send the Javascript only once.
if (!$sent && isset($element['#autocomplete_path']) && !empty($element['#autocomplete_path']) && variable_get('language_negotiation', LANGUAGE_NEGOTIATION_NONE) != LANGUAGE_NEGOTIATION_NONE) {
if (!$sent && isset($element['#autocomplete_path']) && !empty($element['#autocomplete_path']) && variable_get('language_negotiation', LANGUAGE_NEGOTIATION_DEFAULT) != LANGUAGE_NEGOTIATION_DEFAULT) {
// Add a JS file for node forms.
// Determine if we are either editing or translating an existing node.
// We can't act on regular node creation because we don't have a specified
@@ -415,7 +430,16 @@ function i18n_node_form_node_type_form_alter(&$form, &$form_state) {
// Some settings about node languages. Add variables for node type from variable definition
if ($form['#node_type']->type) {
variable_type_include('node_type');
$form['i18n'] += node_variable_type_subform($form['#node_type']->type, array('i18n_node_options', 'i18n_node_extended'));
$form['i18n'] += node_variable_type_subform($form['#node_type']->type, array('i18n_node_options', 'i18n_node_default_language_for', 'i18n_node_extended'));
// Only show custom default language field if "current" is checked.
$form['i18n']['i18n_node_default_language_for']['#states'] = array(
'visible' => array(
':input[name="i18n_node_options[current]"]' => array('checked' => TRUE),
),
'required' => array(
':input[name="i18n_node_options[current]"]' => array('checked' => TRUE),
),
);
}
// Add disabled message
if ($disabled) {
@@ -442,7 +466,7 @@ function i18n_node_form_node_form_alter(&$form, $form_state) {
*
* @see http://drupal.org/node/765860
*/
// Replace core's node submit callback with our own,
// in order to translate the node type name.
$key = array_search('node_form_submit', $form['actions']['submit']['#submit']);
@@ -464,8 +488,7 @@ function i18n_node_form_submit($form, &$form_state) {
$insert = empty($node->nid);
node_save($node);
$node_link = l(t('view'), 'node/' . $node->nid);
$type = node_type_get_type($node->type);
$type_name = i18n_node_translate_type($type);
$type_name = i18n_node_type_name($node->type);
$watchdog_args = array('@type' => $node->type, '%title' => $node->title);
$t_args = array('@type' => $type_name, '%title' => $node->title);
@@ -481,7 +504,7 @@ function i18n_node_form_submit($form, &$form_state) {
if ($node->nid) {
$form_state['values']['nid'] = $node->nid;
$form_state['nid'] = $node->nid;
$form_state['redirect'] = 'node/' . $node->nid;
$form_state['redirect'] = node_access('view', $node) ? 'node/' . $node->nid : '<front>';
}
else {
// In the unlikely case something went wrong on save, the node will be
@@ -505,8 +528,11 @@ function _i18n_node_form_node_form_alter($form, &$form_state) {
}
}
elseif (variable_get('i18n_node_default_language_none', 0) && !isset($form['#node']->nid)) {
// Override locale module setting default language to nodes. It is already in form_state.
$form['language']['#value'] = $form_state['values']['language'] = LANGUAGE_NONE;
// Only do this if the language is really disabled
if (variable_get('language_content_type_' . $node->type, 0) == 0) {
// Override locale module setting default language to nodes. It is already in form_state.
$form['language']['#value'] = $form_state['values']['language'] = LANGUAGE_NONE;
}
}
// Translate field names for title and body for the node edit form.
if (!empty($form['title']['#title'])) {
@@ -516,12 +542,11 @@ function _i18n_node_form_node_form_alter($form, &$form_state) {
$form['body_field']['body']['#title'] = i18n_node_translate_type($node->type, 'body', $form['body_field']['body']['#title']);
}
// Translate page title for node/add/% and node/%/edit pages.
$types = node_type_get_types();
if (empty($node->nid) && strpos($_GET['q'], 'node/add/' . str_replace('_', '-', $node->type)) === 0) {
drupal_set_title(t('Create @name', array('@name' => i18n_node_translate_type($types[$node->type], 'name'))), PASS_THROUGH);
drupal_set_title(t('Create @name', array('@name' => i18n_node_type_name($node->type))), PASS_THROUGH);
}
elseif (!empty($node->nid) && $_GET['q'] == 'node/' . $node->nid . '/edit') {
drupal_set_title(t('<em>Edit @type</em> @title', array('@type' => i18n_node_translate_type($types[$node->type], 'name'), '@title' => $node->title)), PASS_THROUGH);
drupal_set_title(t('<em>Edit @type</em> @title', array('@type' => i18n_node_type_name($node->type), '@title' => $node->title)), PASS_THROUGH);
}
return $form;
}
@@ -553,10 +578,16 @@ function i18n_node_translate_type($type, $property = 'name', $source = NULL, $op
}
/**
* Translate node type name (unfiltered)
* Get translated node type name (unfiltered)
*
* @param string $type
* Node type.
* @param string $name
* Optional node type name.
*/
function i18n_node_type_name($type, $name) {
return i18n_string_translate(array('node', 'type', $type, 'name'), $name);
function i18n_node_type_name($type, $name = NULL) {
$name = isset($name) ? $name : node_type_get_name($type);
return i18n_string_translate(array('node', 'type', $type, 'name'), $name, array('sanitize' => FALSE));
}
/**
@@ -25,7 +25,7 @@ function i18n_node_add_page() {
if ($type) {
// We just need to translate the description, the title is translated by the menu system
// The string will be filtered (xss_admin) on the theme layer
$item['description'] = i18n_node_translate_type($type, 'description', $item['description']);
$item['description'] = i18n_node_translate_type($type, 'description', $item['description'], array('sanitize' => FALSE));
}
}
return theme('node_add_list', array('content' => $content));
@@ -213,6 +213,7 @@ function i18n_node_select_translation_submit($form, &$form_state) {
))
->condition('nid', $add)
->execute();
entity_get_controller('node')->resetCache($add);
if (count($new)) {
drupal_set_message(format_plural(count($new), 'Added a node to the translation set.', 'Added @count nodes to the translation set.'));
}
@@ -224,6 +225,7 @@ function i18n_node_select_translation_submit($form, &$form_state) {
))
->condition('nid', $remove)
->execute();
entity_get_controller('node')->resetCache($remove);
drupal_set_message(format_plural(count($remove), 'Removed a node from the translation set.', 'Removed @count nodes from the translation set.'));
}
}
@@ -246,7 +248,7 @@ function i18n_node_autocomplete($type, $language, $string = '') {
*/
function i18n_node_nid2autocomplete($nid) {
if ($node = node_load($nid)) {
return check_plain($node->title) . ' [nid:' . $nid . ']';
return $node->title . ' [nid:' . $nid . ']';
}
else {
return t('Not found');
@@ -0,0 +1,61 @@
<?php
/**
* @file
* Builds placeholder replacement tokens for content types.
*/
/**
* Implements hook_token_info().
*/
function i18n_node_token_info() {
$content_type['i18n-name'] = array(
'name' => t("Name (localized)"),
'description' => t("The name of the content type."),
);
$content_type['i18n-description'] = array(
'name' => t("Description (localized)"),
'description' => t("The optional description of the content type."),
);
return array(
'tokens' => array(
'content-type' => $content_type,
),
);
}
/**
* Implements hook_tokens().
*/
function i18n_node_tokens($type, $tokens, array $data = array(), array $options = array()) {
$replacements = array();
$sanitize = !empty($options['sanitize']) ? TRUE : FALSE;
$langcode = isset($options['language']) ? $options['language']->language : i18n_langcode();
if ($type == 'content-type' && !empty($data['node_type'])) {
$node_type = $data['node_type'];
foreach ($tokens as $name => $original) {
switch ($name) {
case 'i18n-name':
$name = array('node', 'type', $node_type->type, 'name');
$options = array('sanitize' => $sanitize, 'langcode' => $langcode);
$name = i18n_string_translate($name, $node_type->name, $options);
$replacements[$original] = $name;
break;
case 'i18n-description':
$description = array('node', 'type', $node_type->type, 'description');
$options = array('sanitize' => $sanitize, 'langcode' => $langcode);
$description = i18n_string_translate($description, $node_type->description, $options);
$replacements[$original] = $description;
break;
}
}
}
return $replacements;
}
@@ -45,13 +45,30 @@ function i18n_node_variable_info($options = array()) {
'repeat' => array(
'type' => 'options',
'options' => array(
'current' => t('Set current language as default for new content.', array(), $options),
// Note: this was previously used only to mark new, translatable nodes
// with the current language of the user. Now, this setting is extended
// to allow a specific language to be chosen (defaulting to the current
// language). This was done for backwards compatibility reasons.
'current' => t('Set custom language as default for new content.', array(), $options),
'required' => t('Require language (Do not allow Language Neutral).', array(), $options),
'lock' => t('Lock language (Cannot be changed).', array(), $options),
),
),
'group' => 'i18n',
);
// This field will only be displayed if "current" is checked above.
$variables['i18n_node_default_language_for_[node_type]'] = array(
'type' => 'multiple',
'title' => t('Custom default language', array(), $options),
'repeat' => array(
'type' => 'select',
'options' => array_merge(array(
'-- current --' => t('Current language')
), locale_language_list('name')),
'default' => '-- current --',
),
'group' => 'i18n',
);
$variables['i18n_node_extended_[node_type]'] = array(
'type' => 'multiple',
'title' => t('Extended language support'),
@@ -6,9 +6,8 @@ core = 7.x
files[] = i18n_path.inc
files[] = i18n_path.test
; Information added by drupal.org packaging script on 2013-01-13
version = "7.x-1.8"
; Information added by Drupal.org packaging script on 2020-06-17
version = "7.x-1.27"
core = "7.x"
project = "i18n"
datestamp = "1358075001"
datestamp = "1592373390"
@@ -0,0 +1,82 @@
CONTENTS OF THIS FILE
---------------------
* Introduction
* Requirements
* Recommended modules
* Installation
* Configuration
* Maintainers
INTRODUCTION
------------
The Redirect translation module, part of the Internationalization
(https://www.drupal.org/project/i18n) package, improves search engine
optimization (SEO) for multilingual websites.
It redirects anonymous users (including web crawlers) to the translation of the
page in the requested language, if it exists, using a 301 redirect code.
* For a full description of the module, visit this page:
https://www.drupal.org/node/1280468.
* To submit bug reports and feature suggestions, or to track changes:
https://www.drupal.org/project/issues/i18n.
REQUIREMENTS
------------
This module requires the following module:
* Internationalization - https://www.drupal.org/project/i18n
The Translation redirect module requires the implementation of
hook_i18n_translate_path by another module for the redirect page to be
determined. Currently, the Multilingual content, Path translation, and Taxonomy
translation modules implement this hook.
RECOMMENDED MODULES
-------------------
* Internationalization Views - https://www.drupal.org/project/i18nviews
* Language Icons - https://www.drupal.org/project/languageicons
* Translation Overview - https://www.drupal.org/project/translation_overview
* Localization Client - https://www.drupal.org/project/l10n_client
* Internationalization contributions -
https://www.drupal.org/project/i18n_contrib
INSTALLATION
------------
This is a submodule of the Internationalization module. Install the
Internationalization module as you would normally install a contributed Drupal
module. Visit https://www.drupal.org/node/895232 for further information.
CONFIGURATION
-------------
No configuration is necessary.
For node translation, enable the Multilingual content and the Translation
redirect modules. For non-node pages, the redirection hook must be implemented
by the relevant module.
For example, for taxonomy pages, you should enable the Taxonomy translation
module because the module provides the necessary hook code. If you are using the
Path translation module to create translation sets for non-node pages, then it
implements the hook code for determining the redirection page.
MAINTAINERS
-----------
* Jose Reyero - https://www.drupal.org/u/jose-reyero
* Florian Weber (webflo) - https://www.drupal.org/u/webflo
* Peter Philipp - https://www.drupal.org/u/das-peter
* Joseph Olstad - https://www.drupal.org/u/joseph.olstad
* Nathaniel Catchpole - https://www.drupal.org/u/catch
@@ -4,9 +4,8 @@ dependencies[] = i18n
package = Multilingual - Internationalization
core = 7.x
; Information added by drupal.org packaging script on 2013-01-13
version = "7.x-1.8"
; Information added by Drupal.org packaging script on 2020-06-17
version = "7.x-1.27"
core = "7.x"
project = "i18n"
datestamp = "1358075001"
datestamp = "1592373390"
@@ -0,0 +1,82 @@
CONTENTS OF THIS FILE
---------------------
* Introduction
* Requirements
* Recommended modules
* Installation
* Configuration
* Maintainers
INTRODUCTION
------------
The Multilingual select module, part of the Internationalization
(https://www.drupal.org/project/i18n) package, allows the user to define whether
content is filtered by language on pages provided by Drupal core.
* For a full description of the module visit:
https://www.drupal.org/node/1279512.
* To submit bug reports and feature suggestions, or to track changes visit
https://www.drupal.org/project/issues/i18n.
REQUIREMENTS
------------
This module requires the following module:
* Internationalization - https://www.drupal.org/project/i18n
* Variable - https://www.drupal.org/project/variable
RECOMMENDED MODULES
-------------------
* Internationalization Views - https://www.drupal.org/project/i18nviews
* Language Icons - https://www.drupal.org/project/languageicons
* Translation Overview - https://www.drupal.org/project/translation_overview
* Localization Client - https://www.drupal.org/project/l10n_client
* Internationalization contributions -
https://www.drupal.org/project/i18n_contrib
INSTALLATION
------------
This is a submodule of the Internationalization module. Install the
Internationalization module as you would normally install a contributed Drupal
module. Visit https://www.drupal.org/node/895232 for further information.
CONFIGURATION
-------------
The module allows the user to configure whether or not to filter pages by the
current language. If the Taxonomy translation submodule is also enabled, an
option will be available for how taxonomy term pages are filtered. It also
allows the exclusion of the modules language selection handling for some
elements and certain paths.
1. Enable the Multilingual select module included with Internationalization.
2. Navigate to Configuration > Regional and language > Multilingual settings >
Selection.
3. Select nodes and taxonomy can be filtered by language in the Content to
Filter by Language field set.
4. The "Content Selection Mode" allows content with specific tags to be skipped
by entering a list of tags.
5. The "Enable for Specific Pages" field set allows specific pages to be
included by path.
6. After making choices, Save configuration.
MAINTAINERS
-----------
* Jose Reyero - https://www.drupal.org/u/jose-reyero
* Florian Weber (webflo) - https://www.drupal.org/u/webflo
* Peter Philipp - https://www.drupal.org/u/das-peter
* Joseph Olstad - https://www.drupal.org/u/joseph.olstad
* Nathaniel Catchpole - https://www.drupal.org/u/catch
@@ -6,9 +6,8 @@ core = 7.x
configure = admin/config/regional/i18n/select
files[] = i18n_select.test
; Information added by drupal.org packaging script on 2013-01-13
version = "7.x-1.8"
; Information added by Drupal.org packaging script on 2020-06-17
version = "7.x-1.27"
core = "7.x"
project = "i18n"
datestamp = "1358075001"
datestamp = "1592373390"
@@ -82,11 +82,11 @@ function i18n_select_mode($type = NULL) {
}
/**
* Check current path to enable selection
* Check current path to enable selection.
*
* This works pretty much like block visibility
* This works pretty much like block visibility.
*
* @return boolean
* @return bool
* TRUE if content selection should be enabled for this page.
*/
function i18n_select_page() {
@@ -100,8 +100,11 @@ function i18n_select_page() {
// with different case. Ex: /Page, /page, /PAGE.
$pages = drupal_strtolower($pages);
if ($visibility < I18N_SELECT_PAGE_PHP) {
// Convert the Drupal path to lowercase
$path = drupal_strtolower(drupal_get_path_alias($_GET['q']));
// @see views_ajax()
// @see I18NSelectAdminViewsAjax::testViewsAjaxWithoutSkippingTags()
$path = isset($_REQUEST['view_path']) ? $_REQUEST['view_path'] : $_GET['q'];
// Convert the Drupal path to lowercase.
$path = drupal_strtolower(drupal_get_path_alias($path));
// Compare the lowercase internal and lowercase path alias (if any).
$page_match = drupal_match_path($path, $pages);
if ($path != $_GET['q']) {
@@ -109,8 +112,8 @@ function i18n_select_page() {
}
// When $visibility has a value of 0 (I18N_SELECT_PAGE_NOTLISTED),
// the block is displayed on all pages except those listed in $pages.
// When set to 1 (I18N_SELECT_PAGE_LISTED), it is displayed only on those
// pages listed in $pages.
// When set to 1 (I18N_SELECT_PAGE_LISTED), it is displayed only on
// those pages listed in $pages.
$mode = !($visibility xor $page_match);
}
elseif (module_exists('php')) {
@@ -121,7 +124,7 @@ function i18n_select_page() {
}
}
else {
// No pages defined, still respect the setting (unlike blocks)
// No pages defined, still respect the setting (unlike blocks).
$mode = $visibility == I18N_SELECT_PAGE_NOTLISTED;
}
}
@@ -223,7 +226,7 @@ function _i18n_select_table_alias($table) {
function i18n_select_check_conditions($query, $table_alias = NULL) {
$conditions =& $query->conditions();
foreach ($conditions as $index => $condition) {
if (is_array($condition) && isset($condition['field'])) {
if (is_array($condition) && !empty($condition['field'])) {
if (strpos($condition['field'], '.') === FALSE) {
if ($table_alias) {
// Change the condition to include a table alias.
@@ -84,3 +84,87 @@ class i18nSelectTestCase extends Drupali18nTestCase {
}
}
}
/**
* Test case for AJAX queries on "views/ajax" when view on admin page.
*/
class I18NSelectAdminViewsAjax extends Drupali18nTestCase {
/**
* {@inheritdoc}
*/
public static function getInfo() {
return array(
'name' => t('I18N select Admin Views (AJAX)'),
'group' => 'Internationalization',
'description' => t('Test AJAX requests to the "views/ajax" when view located on "admin/*" and list of skipping tags is empty.'),
// Skip this test when "admin_views" module does not exists.
'dependencies' => array('admin_views'),
);
}
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp('translation', 'i18n_variable', 'i18n_select', 'admin_views');
parent::setUpLanguages(array('access content overview'));
parent::setUpContentTranslation();
}
/**
* Test AJAX of a view without skipping tags for selection.
*
* @see i18n_select_page()
*/
public function testViewsAjaxWithoutSkippingTags() {
// If this variable will have the "views" value then this test will not
// have sense. For instance, we want apply language selection filter
// for views and remove "views" from "i18n_select_skip_tags" variable.
// In this case all AJAX for views, on administration part of the site,
// will be broken because the "i18n_select_page()" function will work
// with "views/ajax" path instead of, for example, "admin/content".
variable_set('i18n_select_skip_tags', '');
// Create one hundred of nodes.
for ($i = 1; $i <= 100; $i++) {
// Create every second node on Spanish language and
// every first - on English.
$node = $this->createNode('page', "Node $i", '', $i % 2 ? $this->default_language : $this->secondary_language);
// Update "changed" in order to sort the content by updating date. In
// other case all nodes will be with the same date and not arranged in
// order.
db_update('node')
->fields(array('changed' => strtotime("+ $i minute")))
->condition('nid', $node->nid)
->execute();
}
$this->drupalGet('admin/content');
// Check that latest node exists at the top.
$this->assertText('Node 100');
// Check that our page contains fifty nodes (the latest must be 51).
$this->assertNoText('Node 50');
// Test $_REQUEST['view_path']. There's no form to submit to, so
// drupalPost() won't work here. This just tests a direct $_POST
// request instead.
$this->curlExec(array(
CURLOPT_URL => $this->getAbsoluteUrl('views/ajax'),
CURLOPT_POST => TRUE,
CURLOPT_POSTFIELDS => http_build_query(array(
'page' => 1,
'view_path' => 'admin/content',
'view_name' => 'admin_views_node',
'view_display_id' => 'system_1',
)),
));
// Check that we are successfully switched to a new page of content.
$this->assertText('Node 50');
$this->assertNoText('Node 100');
}
}
@@ -0,0 +1,94 @@
CONTENTS OF THIS FILE
---------------------
* Introduction
* Requirements
* Recommended modules
* Installation
* Configuration
* Maintainers
INTRODUCTION
------------
The String translation module, part of the Internationalization
(https://www.drupal.org/project/i18n) package, provides support for other
modules to translate user-defined strings. This is an API module that must be
enabled only when required by other modules in the i18n package.
* For a full description of the module, visit this page:
https://www.drupal.org/node/1279668
* To submit bug reports and feature suggestions, or to track changes:
https://www.drupal.org/project/issues/i18n
REQUIREMENTS
------------
This module requires the following module:
* Internationalization - https://www.drupal.org/project/i18n
RECOMMENDED MODULES
-------------------
* Internationalization Views - https://www.drupal.org/project/i18nviews
* Language Icons - https://www.drupal.org/project/languageicons
* Translation Overview - https://www.drupal.org/project/translation_overview
* Localization Client - https://www.drupal.org/project/l10n_client
* Internationalization contributions -
https://www.drupal.org/project/i18n_contrib
INSTALLATION
------------
This is a submodule of the Internationalization module. Install the
Internationalization module as you would normally install a contributed Drupal
module. Visit https://www.drupal.org/node/895232 for further information.
CONFIGURATION
-------------
Strings will be translated from the source languages. By default the source
language is the site's default language, so changing the default language could
break these translations.
1. The user can set which language is used as the source language via
Administration > Configuration > Regional and language > Multilingual
settings > Strings. By default, only plain strings are enabled, so regular
blocks are not fully translatable.
2. To allow Filtered HTML, Full HTML or Plain text select the appropriate radio
box(es) and Save configuration.
3. To select the strings to be translated navigate to Administration >
Configuration > Regional and language > Translate interface and select on
Stings vertical tab. From here the user can select which text groups to
translate and select the Refresh strings tab.
FAQ
---
The String translation module allows you to configure which text formats are
translatable. Formats like PHP Filter and Full HTML are translated before they
are processed, so allowing a translator to edit these can be a security risk.
This is particularly problematic when importing translations in bulk from a CSV
file, since the translator's access to the import formats isn't verified by
Drupal. After updating this setting, be sure to refresh the strings via
Administration > Configuration > Regional and language > Translate interface >
Strings so that strings in forbidden formats are deleted.
MAINTAINERS
-----------
* Jose Reyero - https://www.drupal.org/u/jose-reyero
* Florian Weber (webflo) - https://www.drupal.org/u/webflo
* Peter Philipp - https://www.drupal.org/u/das-peter
* Joseph Olstad - https://www.drupal.org/u/joseph.olstad
* Nathaniel Catchpole - https://www.drupal.org/u/catch
@@ -28,7 +28,7 @@ class i18n_string_object {
// Properties from metadata
public $title;
// Array of translations to multiple languages
public $translations;
public $translations = array();
// Textgroup object
protected $_textgroup;
@@ -39,7 +39,49 @@ class i18n_string_object {
if ($data) {
$this->set_properties($data);
}
// Attempt to re-build the data from the persistent cache.
$this->rebuild_from_cache($data);
}
/**
* Rebuild the object data based on the persistent cache.
*
* Since the textgroup defines if a string is cacheable or not the caching
* of the string objects happens in the textgroup handler itself.
*
* @see i18n_string_textgroup_cached::__destruct()
*/
protected function rebuild_from_cache($data = NULL) {
// Check if we've the required information to repopulate the cache and do so
// if possible.
$meta_data_exist = isset($this->textgroup) && isset($this->type) && isset($this->objectid) && isset($this->property);
if ($meta_data_exist && ($cache = cache_get($this->get_cid())) && !empty($cache->data)) {
// Re-spawn the cached data.
// @TODO do we need a array_diff to ensure we don't overwrite the data
// provided by the $data parameter?
$this->set_properties($cache->data);
}
}
/**
* Reset cache, needed for tests.
*/
public function cache_reset() {
$this->translations = array();
// Ensure a possible persistent cache of this object is cleared too.
cache_clear_all($this->get_cid(), 'cache', TRUE);
}
/**
* Returns the caching id for this object.
*
* @return string
* The caching id.
*/
public function get_cid() {
return 'i18n:string:obj:' . $this->get_name();
}
/**
* Get message parameters from context and string.
*/
@@ -63,6 +105,10 @@ class i18n_string_object {
$this->objectkey = (int)$this->objectid;
// Remaining elements glued again with ':'
$this->property = $parts ? implode(':', $parts) : '';
// Attempt to re-build the other data from the persistent cache.
$this->rebuild_from_cache();
return $this;
}
/**
@@ -100,13 +146,14 @@ class i18n_string_object {
if (isset($string['format'])) {
$this->format = $string['format'];
}
if (isset($string['title'])) {
$this->title = $string['title'];
}
}
else {
$this->string = $string;
}
if (isset($string['title'])) {
$this->title = $string['title'];
}
return $this;
}
/**
@@ -279,9 +326,9 @@ class i18n_string_textgroup_default {
// Debug flag, set to true to print out more information.
public $debug;
// Cached or preloaded string objects
public $strings;
public $strings = array();
// Multiple translations search map
protected $cache_multiple;
protected $cache_multiple = array();
/**
* Class constructor.
@@ -400,8 +447,13 @@ class i18n_string_textgroup_default {
/**
* Filter array of strings
*
* @param $filter
* @param array $string_list
* Array of strings to be filtered.
* @param array $filter
* Array of name value conditions.
*
* @return array
* Strings from $string_list that match the filter conditions.
*/
protected static function string_filter($string_list, $filter) {
// Remove 'language' and '*' conditions.
@@ -566,7 +618,11 @@ class i18n_string_textgroup_default {
public function cache_reset() {
$this->strings = array();
$this->string_format = array();
$this->translations = array();
// Reset the persistent caches.
cache_clear_all('i18n:string:tgroup:' . $this->textgroup , 'cache', TRUE);
// Reset the complete string object cache too.
cache_clear_all('i18n:string:obj:', 'cache', TRUE);
}
/**
@@ -613,7 +669,7 @@ class i18n_string_textgroup_default {
public static function load_translation($i18nstring, $langcode) {
// Search the database using lid if we've got it or textgroup, context otherwise
if (!empty($i18nstring->lid)) {
// We've alreay got lid, we just need translation data
// We've already got lid, we just need translation data
$query = db_select('locales_target', 't');
$query->condition('t.lid', $i18nstring->lid);
}
@@ -647,9 +703,19 @@ class i18n_string_textgroup_default {
// Create source string so we get an lid
$this->save_source($string);
}
// Convert objectid to objectkey if it's numeric.
if (!isset($string->objectkey)) {
$string->objectkey = (int)$string->objectid;
if (is_numeric($string->objectid)) {
$string->objectkey = (int)$string->objectid;
}
}
// Make sure objectkey is numeric.
if (!is_numeric($string->objectkey)) {
$string->objectkey = 0;
}
if (!isset($string->format)) {
$string->format = '';
}
@@ -1110,10 +1176,17 @@ class i18n_string_object_wrapper extends i18n_object_wrapper {
$info = is_array($info) ? $info : array('title' => $info);
$field_name = isset($info['field']) ? $info['field'] : $field;
$value = $this->get_field($field_name);
if (is_array($value) && isset($value['value'])) {
$format = isset($value['format']) ? $value['format'] : NULL;
$value = $value['value'];
}
else {
$format = isset($info['format']) ? $this->get_field($info['format']) : NULL;
}
$strings[$this->get_textgroup()][$string_type][$object_id][$field] = array(
'string' => is_array($value) || isset($info['empty']) && $value === $info['empty'] ? NULL : $value,
'title' => $info['title'],
'format' => isset($info['format']) ? $this->get_field($info['format']) : NULL,
'format' => $format,
'name' => array_merge($object_keys, array($field)),
);
}
@@ -1188,6 +1261,10 @@ class i18n_string_object_wrapper extends i18n_object_wrapper {
* Translate access (localize strings)
*/
protected function localize_access() {
// We could check also whether the object has strings to translate:
// && $this->get_strings(array('empty' => TRUE))
// However it may be better to display the 'No available strings' message
// for the user to have a clue of what's going on. See i18n_string_translate_page_object()
return user_access('translate interface') && user_access('translate user-defined strings');
}
@@ -1280,3 +1357,168 @@ class i18n_string_object_wrapper extends i18n_object_wrapper {
return $this->textgroup()->load_strings(array('type' => $type, 'objectid' => $id));
}
}
/**
* Textgroup handler for i18n_string API which integrated persistent caching.
*/
class i18n_string_textgroup_cached extends i18n_string_textgroup_default {
/**
* Defines the timeout for the persistent caching.
* @var int
*/
public $caching_time = CACHE_TEMPORARY;
/**
* Extends the existing constructor with a cache handling.
*
* @param string $textgroup
* The name of this textgroup.
*/
public function __construct($textgroup) {
parent::__construct($textgroup);
// Fetch persistent caches, the persistent caches contain only metadata.
// Those metadata are processed by the related cache_get() methods.
foreach (array('cache_multiple', 'strings') as $caches_type) {
if (($cache = cache_get('i18n:string:tgroup:' . $this->textgroup . ':' . $caches_type)) && !empty($cache->data)) {
$this->{$caches_type} = $cache->data;
}
}
}
/**
* Class destructor.
*
* Updates the persistent caches for the next usage.
* This function not only stores the data of the textgroup objects but also
* of the string objects. That way we ensure that only cacheable string object
* go into the persistent cache.
*/
public function __destruct() {
// Reduce size to cache by removing NULL values.
$this->strings = array_filter($this->strings);
$strings_to_cache = array();
// Store the persistent caches. We just store the metadata the translations
// are stored by the string object itself. However storing the metadata
// reduces the number of DB queries executed during runtime.
$cache_data = array();
foreach ($this->strings as $context => $i18n_string_object) {
$cache_data[$context] = $context;
$strings_to_cache[$context] = $i18n_string_object;
}
cache_set('i18n:string:tgroup:' . $this->textgroup . ':strings', $cache_data, 'cache', $this->caching_time);
$cache_data = array();
foreach ($this->cache_multiple as $pattern => $strings) {
foreach ($strings as $context => $i18n_string_object) {
$cache_data[$pattern][$context] = $context;
$strings_to_cache[$context] = $i18n_string_object;
}
}
cache_set('i18n:string:tgroup:' . $this->textgroup . ':cache_multiple', $cache_data, 'cache', $this->caching_time);
// Cache the string objects related to this textgroup.
// Store only the public visible data into the persistent cache.
foreach ($strings_to_cache as $i18n_string_object) {
// If this isn't an object it's an unprocessed cache item and doesn't need
// to be stored again.
if (is_object($i18n_string_object)) {
cache_set($i18n_string_object->get_cid(), get_object_vars($i18n_string_object), 'cache', $this->caching_time);
}
}
}
/**
* Reset cache, needed for tests.
*
* Takes care of the persistent caches.
*/
public function cache_reset() {
// Reset the persistent caches.
cache_clear_all('i18n:string:tgroup:' . $this->textgroup , 'cache', TRUE);
// Reset the complete string object cache too. This will affect string
// objects of other textgroups as well.
cache_clear_all('i18n:string:obj:', 'cache', TRUE);
return parent::cache_reset();
}
/**
* Get translation from cache.
*
* Extends the original handler with persistent caching.
*
* @param string $context
* The context to look out for.
* @return i18n_string_object|NULL
* The string object if available or NULL otherwise.
*/
protected function cache_get($context) {
if (isset($this->strings[$context])) {
// If the cache contains a string re-build i18n_string_object.
if (is_string($this->strings[$context])) {
$i8n_string_object = new i18n_string_object(array('textgroup' => $this->textgroup));
$i8n_string_object->set_context($context);
$this->strings[$context] = $i8n_string_object;
}
// Now run the original handling.
return parent::cache_get($context);
}
return NULL;
}
/**
* Get strings from multiple cache.
*
* @param $context array
* String context as array with language property at the end.
*
* @return mixed
* Array of strings (may be empty) if we've got a cache hit.
* Null otherwise.
*/
protected function multiple_cache_get($context) {
// Ensure the values from the persistent cache are properly re-build.
$cache_key = implode(':', $context);
if (isset($this->cache_multiple[$cache_key])) {
foreach ($this->cache_multiple[$cache_key] as $cached_context) {
if (is_string($cached_context)) {
$i8n_string_object = new i18n_string_object(array('textgroup' => $this->textgroup));
$i8n_string_object->set_context($cached_context);
$this->cache_multiple[$cache_key][$cached_context] = $i8n_string_object;
}
}
}
else {
// Now we try more generic keys. For instance, if we are searching 'term:1:*'
// we may try too 'term:*:*' and filter out the results.
foreach ($context as $key => $value) {
if ($value != '*') {
$try = array_merge($context, array($key => '*'));
$cached_results = $this->multiple_cache_get($try);
// Now filter the ones that actually match.
if (!empty($cached_results)) {
$cached_results = $this->string_filter($cached_results, $context);
}
return $cached_results;
}
}
}
return parent::multiple_cache_get($context);
}
public function string_update($i18nstring, $options = array()) {
// Flush persistent cache.
cache_clear_all($i18nstring->get_cid(), 'cache', TRUE);
return parent::string_update($i18nstring, $options);
}
public function string_remove($i18nstring, $options = array()) {
// Flush persistent cache.
cache_clear_all($i18nstring->get_cid(), 'cache', TRUE);
return parent::string_remove($i18nstring, $options);
}
}
@@ -10,9 +10,8 @@ files[] = i18n_string.inc
files[] = i18n_string.test
configure = admin/config/regional/i18n/strings
; Information added by drupal.org packaging script on 2013-01-13
version = "7.x-1.8"
; Information added by Drupal.org packaging script on 2020-06-17
version = "7.x-1.27"
core = "7.x"
project = "i18n"
datestamp = "1358075001"
datestamp = "1592373390"
@@ -28,6 +28,10 @@ function i18n_string_install() {
i18n_string_update_7000();
i18n_string_update_7001();
}
// Create new index in {locales_source}, performance improvement in sites with i18n.
if (!db_index_exists('locales_source', 'textgroup_context')) {
db_add_index('locales_source', 'textgroup_context', array('textgroup', array('context', 50)));
}
}
/**
@@ -36,6 +40,8 @@ function i18n_string_install() {
function i18n_string_uninstall() {
// Drop custom field.
db_drop_field('locales_target', 'i18n_status');
// Drop custom index in locales_source table
db_drop_index('locales_source', 'textgroup_context');
}
/**
@@ -88,6 +94,7 @@ function i18n_string_schema() {
),
'objectindex' => array(
'type' => 'int',
'size' => 'big',
'not null' => TRUE,
'default' => 0,
'description' => 'Integer value of Object ID.',
@@ -102,7 +109,7 @@ function i18n_string_schema() {
),
'primary key' => array('lid'),
'indexes' => array(
'group_context' => array('textgroup', 'context'),
'group_context' => array('textgroup', array('context', 50)),
),
);
return $schema;
@@ -230,6 +237,34 @@ function i18n_string_update_7001() {
}
}
/**
* Create new index in {locales_source}, performance improvement in sites with i18n.
*/
function i18n_string_update_7002() {
if (!db_index_exists('locales_source', 'textgroup_context')) {
db_add_index('locales_source', 'textgroup_context', array('textgroup', array('context', 50)));
}
}
/**
* Removed due to buggy upgrade for #2200647.
*/
function i18n_string_update_7003() {
}
/**
* Change objectindex from int to bigint.
*/
function i18n_string_update_7004() {
db_change_field('i18n_string', 'objectindex', 'objectindex', array(
'type' => 'int',
'size' => 'big',
'not null' => TRUE,
'default' => 0,
'description' => 'Integer value of Object ID.',
));
}
/**
* Notes for update script
*/
@@ -257,4 +292,4 @@ function i18n_string_update_7001() {
* Node type
* nodetype:type:[type]:[property] -> node:type:[type]:[property]
* Property names: title -> title_label
*/
*/
@@ -118,13 +118,14 @@ function i18n_string_menu() {
'file' => 'i18n_string.admin.inc',
'access arguments' => array('translate interface'),
);
$items['admin/config/regional/i18n/strings'] = array(
'title' => 'Strings',
'description' => 'Options for user defined strings.',
'weight' => 20,
'type' => MENU_LOCAL_TASK,
'page callback' => 'drupal_get_form',
'page arguments' => array('variable_edit_form', array('i18n_string_allowed_formats', 'i18n_string_source_language')),
'page arguments' => array('variable_edit_form', array('i18n_string_allowed_formats', 'i18n_string_source_language', 'i18n_string_textgroup_class_[textgroup]')),
'access arguments' => array('administer site configuration'),
);
// AJAX callback path for strings.
@@ -258,6 +259,34 @@ function i18n_string_locale_translate_import_form_submit($form, &$form_state) {
}
}
/**
* Implements hook_element_info_alter().
*
* We need to do this on the element info level as wysiwyg also does so and form
* API (incorrectly) does not merge in the defaults for values that are arrays.
*/
function i18n_string_element_info_alter(&$types) {
$types['text_format']['#pre_render'][] = 'i18n_string_pre_render_text_format';
}
/**
* The '#pre_render' function to alter the text format element in a translation.
* The text format for a translation is taken form the original, so the text
* format drop down should be disabled.
*
* @param array $element
* The text_format element which will be rendered.
*
* @return array
* The altered text_format element with a disabled "Text format" select.
*/
function i18n_string_pre_render_text_format($element) {
if (!empty($element['#i18n_string_is_translation'])) {
$element['format']['format']['#attributes']['disabled'] = TRUE;
}
return $element;
}
/**
* Check if translation is required for this language code.
*
@@ -334,7 +363,10 @@ function i18n_string_update_context($oldname, $newname) {
}
/**
* Get textgroup handler
* Get textgroup handler.
*
* @return i18n_string_textgroup_default
*
*/
function i18n_string_textgroup($textgroup) {
$groups = &drupal_static(__FUNCTION__);
@@ -488,6 +520,19 @@ function i18n_string_group_info($group = NULL, $property = NULL, $default = NULL
}
}
/**
* Implements hook_i18n_string_info_alter().
*
* Set determined classes to use for the text group.
*/
function i18n_string_i18n_string_info_alter(&$info) {
foreach (array_keys($info) as $name) {
// If class is not defined. Classes from other modules, fixed classes and etc.
if (!isset($info[$name]['class'])) {
$info[$name]['class'] = variable_get('i18n_string_textgroup_class_' . $name, 'i18n_string_textgroup_default');
}
}
}
/**
* Translate / update multiple strings
@@ -519,11 +564,12 @@ function i18n_string_multiple($operation, $name, $strings, $options = array()) {
*
* This function is intended to return translations for plain strings that have NO text format
*
* @param $name
* @param array|string name
* Array or string concatenated with ':' that contains textgroup and string context
* @param $string
* String in default language or array of strings to be translated
* @param $options
* @param array|string $string
* A string in the default language, a string wth format (array with keys
* value and format),or an array of strings (without format) to be translated.
* @param array $options
* An associative array of additional options, with the following keys:
* - 'langcode' (defaults to the current language) The language code to translate to a language other than what is used to display the page.
* - 'filter' Filtering callback to apply to the translated string only
@@ -531,8 +577,13 @@ function i18n_string_multiple($operation, $name, $strings, $options = array()) {
* - 'callback' Callback to apply to the result (both to translated or untranslated string
* - 'sanitize' Whether to filter the translation applying the text format if any, default is TRUE
* - 'sanitize default' Whether to filter the default value if no translation found, default is FALSE
*
* @return string
*/
function i18n_string_translate($name, $string, $options = array()) {
if (is_array($string) && isset($string['value'])) {
$string = $string['value'];
}
if (is_array($string)) {
return i18n_string_translate_list($name, $string, $options);
}
@@ -583,10 +634,17 @@ function i18n_string_translate_access($string_format, $account = NULL) {
* Message if the user cannot translate that string.
*/
function i18n_string_translate_check_string($i18nstring, $account = NULL) {
if (!user_access('translate interface', $account) || !user_access('translate user-defined strings', $account)) {
// Check block translation permissions.
if ($i18nstring->textgroup == 'blocks') {
if (!user_access('translate interface', $account) && !user_access('translate blocks', $account)) {
return t('This is a user-defined string within a block. You are not allowed to translate blocks.');
}
}
elseif (!user_access('translate interface', $account) || !user_access('translate user-defined strings', $account)) {
return t('This is a user-defined string. You are not allowed to translate these strings.');
}
elseif (!empty($i18nstring->format)) {
if (!empty($i18nstring->format)) {
if (!i18n_string_allowed_format($i18nstring->format)) {
$format = filter_format_load($i18nstring->format);
return t('This string uses the %name text format. Strings with this format are not allowed for translation.', array('%name' => $format->name));
@@ -12,7 +12,14 @@ include_once DRUPAL_ROOT . '/includes/locale.inc';
include_once drupal_get_path('module', 'locale') . '/locale.admin.inc';
/**
* Generate translate page from object
* Generate translate page from object.
*
* @param string $object_type
* Obejct type as declared in hook_i18n_object_info().
* @param object $object_value
* Drupal object to translate.
* @param object $language
* Optional language object.
*/
function i18n_string_translate_page_object($object_type, $object_value, $language = NULL) {
// For backwards compatibility, ensure parameter is a language object
@@ -22,6 +29,13 @@ function i18n_string_translate_page_object($object_type, $object_value, $languag
$object = i18n_object($object_type, $object_value);
$strings = $object->get_strings(array('empty' => TRUE));
// If no localizable strings, print message and fail gracefully.
// Possibly this object comes from some other contrib module.
// See http://drupal.org/node/1889878
if (!$strings) {
return t('This object has no strings available for translation.');
}
if (empty($langcode)) {
drupal_set_title(t('Translate !name', array('!name' => i18n_object_info($object_type, 'title'))));
return i18n_string_translate_page_overview($object, $strings);
@@ -44,7 +58,6 @@ function i18n_string_translate_page_overview($object, $strings) {
* Provide a core translation module like overview page for this object.
*/
function i18n_string_translate_page_overview_form($form, &$form_state, $object, $strings) {
//include_once DRUPAL_ROOT . '/includes/language.inc';
// Set the default item key, assume it's the first.
$item_title = reset($strings);
$header = array(
@@ -157,14 +170,16 @@ function i18n_string_translate_page_form_base($form, $langcode, $redirect = NULL
/**
* Create field elements for strings
*
* @param i18n_string_object[] $strings
* @param string $langcode
*
* @return array
*/
function i18n_string_translate_page_form_strings($strings, $langcode) {
$formats = filter_formats();
global $user;
$form = array();
foreach ($strings as $item) {
// We may have a source or not. Load it, our string may get the format from it.
$source = $item->get_source();
$format_id = $source ? $source->format : $item->format;
$description = '';
// Check permissions to translate this string, depends on format, etc..
if ($message = $item->check_translate_access()) {
// We'll display a disabled element with the reason it cannot be translated.
@@ -175,27 +190,31 @@ function i18n_string_translate_page_form_strings($strings, $langcode) {
$disabled = FALSE;
$description = '';
// If we don't have a source and it can be translated, we create it.
if (!$source) {
if (!$item->get_source()) {
// Enable messages just as a reminder these strings are not being updated properly.
$status = $item->update(array('messages' => TRUE));
if ($status === FALSE || $status === SAVED_DELETED) {
// We don't have a source string so nothing to translate here
$disabled = TRUE;
}
else {
$source = $item->get_source();
}
}
}
$default_value = $item->format_translation($langcode, array('langcode' => $langcode, 'sanitize' => FALSE, 'debug' => FALSE));
$available_formats = array_keys(filter_formats($user));
if (!in_array($item->format, $available_formats)) {
$item->format = NULL;
}
$form[$item->get_name()] = array(
'#title' => $item->get_title(),
'#type' => 'textarea',
'#type' => $item->format ? 'text_format' : 'textarea',
'#default_value' => $default_value,
'#format' => $item->format,
// This will trigger i18n_string_pre_render_text_format() to actually
// alter the element.
'#i18n_string_is_translation' => TRUE,
'#disabled' => $disabled,
'#description' => $description . _i18n_string_translate_format_help($format_id),
//'#i18n_string_format' => $source ? $source->format : 0,
'#description' => $description,
// If disabled, provide smaller textarea (that can be expanded anyway).
'#rows' => $disabled ? 1 : min(ceil(str_word_count($default_value) / 12), 10),
// Change the parent for disabled strings so we don't get empty values later
@@ -213,6 +232,16 @@ function i18n_string_translate_page_form_submit($form, &$form_state) {
foreach ($form_state['values']['strings'] as $name => $value) {
$count++;
list($textgroup, $context) = i18n_string_context(explode(':', $name));
if (is_array($value)) {
if (isset($value['value'])) {
$value = $value['value'];
$form_state['values']['strings'][$name] = $value;
}
else {
form_set_error("strings][$name", t('Unable to get the translated string value.'));
watchdog('locale', 'Unable to get the translated string value, string array is: %string', array('%string' => var_dump($value)), WATCHDOG_WARNING);
}
}
$result = i18n_string_textgroup($textgroup)->update_translation($context, $form_state['values']['langcode'], $value);
$success += ($result ? 1 : 0);
}
@@ -385,6 +414,9 @@ function i18n_string_locale_translate_edit_form_submit($form, &$form_state) {
// Invoke locale submission.
locale_translate_edit_form_submit($form, $form_state);
$lid = $form_state['values']['lid'];
if ($i18n_string_object = i18n_string_get_by_lid($lid)) {
$i18n_string_object->cache_reset();
}
foreach ($form_state['values']['translations'] as $key => $value) {
if (!empty($value)) {
// An update has been made, so we assume the translation is now current.
@@ -24,7 +24,7 @@ class i18nStringTestCase extends Drupali18nTestCase {
function setUp() {
// We can use any of the modules that define a text group, to use it for testing
parent::setUp('i18n_string', 'i18n_menu');
parent::setUp('i18n_string', 'i18n_menu', 'i18n_test');
parent::setUpLanguages();
$this->translator = $this->drupalCreateUser(array('translate interface', 'translate user-defined strings'));
}
@@ -53,8 +53,130 @@ class i18nStringTestCase extends Drupali18nTestCase {
$this->assertEqual($translation, $translations[$key][$language->language], "The right $language->name ($language->language) translation has been retrieved for $name, $translation");
}
}
// Test that regular strings can be translated. Use 'Built-in interface' as
// filter, and translate first one.
$search = array(
'language' => 'all',
'translation' => 'all',
'group' => 'default',
'string' => '',
);
$this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
$this->clickLink(t('edit'));
// Just add a random translation.
$translation = $this->randomName();
$edit = array();
foreach ($this->getOtherLanguages() as $language) {
$langcode = $language->language;
$edit["translations[$langcode]"] = $translation;
}
$this->drupalPost(NULL, $edit, t('Save translations'));
$this->assertText(t('The string has been saved.'), t('The string has been saved.'));
$this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), t('Correct page redirection.'));
}
/**
* Test base i18n_string caching.
*/
function testCaching() {
// Create a bunch of strings for all languages.
$textgroup = 'test_cached';
$strings = $this->stringCreateArray(2);
$translations = array();
$textgroup_object = i18n_string_textgroup($textgroup);
// Save source strings and store translations.
foreach ($strings as $key => $string) {
$name = "$textgroup:item:$key:title";
i18n_string_update($name, $string);
$translations[$key] = $this->createStringTranslation($textgroup, $string);
}
// Now fetch the strings to fill the cache.
foreach ($textgroup_object->strings as $context => $string_object) {
$this->drupalGet('tests/i18n/i18n_string_build/' . $textgroup . ':' . $context);
}
foreach ($strings as $key => $string) {
$this->drupalGet('tests/i18n/i18n_string_translation_search/' . $textgroup . ':item:' . $key . ':*/es');
}
// Check the persistent cache for contents.
$cache = cache_get('i18n:string:tgroup:' . $textgroup . ':strings');
if ($this->assertNotEqual($cache, FALSE, 'Textgroup strings cache found')) {
foreach ($textgroup_object->strings as $context => $string_object) {
if ($this->assertTrue(isset($cache->data[$context]), format_string('Cached string %context found', array('%context' => $context)))) {
$this->assertEqual($cache->data[$context], $context, 'Cached string is a string and not an object');
}
// Check if the string object cache is also available.
$string_cache = cache_get($string_object->get_cid());
if ($this->assertNotEqual($string_cache, FALSE, format_string('Cached string object %cid found', array('%cid' => $string_object->get_cid())))) {
$this->assertTrue(is_array($string_cache->data), 'Cached string object is an array.');
}
}
}
$cache = cache_get('i18n:string:tgroup:' . $textgroup . ':cache_multiple');
if ($this->assertNotEqual($cache, FALSE, 'Textgroup cache_multiple cache found')) {
foreach ($strings as $key => $string) {
$pattern = 'item:' . $key . ':*:es';
if ($this->assertTrue(isset($cache->data[$pattern]), format_string('Cached multiple cache for pattern %pattern found', array('%pattern_key' => $pattern)))) {
$property_pattern = 'item:' . $key . ':title';
if ($this->assertTrue(isset($cache->data[$pattern][$property_pattern]), format_string('Cached multiple property title found', array('%pattern_key' => $pattern)))) {
$this->assertEqual($cache->data[$pattern][$property_pattern], $property_pattern);
}
}
}
}
// Test cache injection.
foreach ($textgroup_object->strings as $context => $string_object) {
// Check if the string object cache is also available.
$string_cache = cache_get($string_object->get_cid());
if (isset($string_cache->data)) {
// Modify cache.
$string_cache->data['string'] = "Injected value.";
cache_set($string_object->get_cid(), $string_cache->data, 'cache', CACHE_TEMPORARY);
// Check if modification is reflected on the next page call.
$this->drupalGet('tests/i18n/i18n_string_build/' . $textgroup . ':' . $context);
$this->assertText($string_cache->data['string']);
}
}
// Test that un-translated strings are cached correctly.
$textgroup = 'test_cached';
$key = 3;
$string = self::randomName(100);
$name = "$textgroup:item:$key:title";
i18n_string_update($name, $string);
// Generate the cache entry.
$string_object = i18n_string_build($name, $string);
$langcode = i18n_langcode();
$string_object->get_translation($langcode);
// Destroy the textgroup object to write the cache entry.
$textgroup_object = i18n_string_textgroup($textgroup);
$textgroup_object->__destruct();
$this->assertTrue(cache_get($string_object->get_cid()) !== FALSE, "Cache entry created.");
drupal_static_reset('i18n_string_textgroup');
// Reset the loaded translation variable.
variable_del('i18n_loaded_translations');
$loaded_translations = variable_get('i18n_loaded_translations', array());
$this->verbose(var_export($loaded_translations, TRUE));
// Rebuild the string.
$string_object = i18n_string_build($name, $string);
$string_object->get_translation($langcode);
// Check that the string hasn't been loaded.
$loaded_translations = variable_get('i18n_loaded_translations', array());
$this->verbose(var_export($loaded_translations, TRUE));
$this->assertFalse(isset($loaded_translations['test_cached:item:3:title']), "The untranslated string was correctly cached.");
}
/**
* Create strings for all languages
*/
@@ -76,7 +198,7 @@ class i18nStringTestCase extends Drupali18nTestCase {
/**
* Create and store one translation into the db
*/
public static function stringCreateTranslation($name, $lang, $length = 20) {
public function stringCreateTranslation($name, $lang, $length = 20) {
$translation = $this->randomName($length);
if (self::stringSaveTranslation($name, $lang, $translation)) {
return $translation;
@@ -34,9 +34,32 @@ function i18n_string_variable_info($options = array()) {
'default' => 0,
'group' => 'debug',
);
$variables['i18n_string_textgroup_class_[textgroup]'] = array(
'title' => t('Class to use for the text group'),
'description' => t('Determines which the class will be use for string translation in the text group.', array(), $options),
'repeat' => array(
'type' => 'select',
'default' => 'i18n_string_textgroup_default',
'options callback' => 'i18n_string_variable_textgroup_class_list',
),
'submit callback' => 'i18n_string_variable_textgroup_class_submit_callback',
'group' => 'i18n',
);
return $variables;
}
/**
* Implements hook_variable_type_info().
*/
function i18n_string_variable_type_info() {
$type['textgroup'] = array(
'title' => t('Text group'),
'type' => 'select',
'options callback' => 'i18n_string_variable_textgroup_list',
);
return $type;
}
/**
* Options callback, format list
*/
@@ -54,4 +77,33 @@ function i18n_string_variable_format_list() {
*/
function i18n_string_variable_format_default() {
return array(filter_fallback_format());
}
}
/**
* Options callback, text groups list.
*/
function i18n_string_variable_textgroup_list() {
$groups = array();
foreach (i18n_string_group_info() as $name => $info) {
$groups[$name] = $info['title'];
}
return $groups;
}
/**
* Options callback, text group classes list.
*/
function i18n_string_variable_textgroup_class_list($variable, $options = array()) {
return array(
'i18n_string_textgroup_default' => t('Text group handler default.', array(), $options),
'i18n_string_textgroup_cached' => t('Text group handler which include persistent caching.', array(), $options),
);
}
/**
* Submit callback. Execute Reset the persistent caches after save the text group class variables.
*/
function i18n_string_variable_textgroup_class_submit_callback($variable, $options, $form, $form_state) {
// Reset the persistent caches.
cache_clear_all('i18n:string:' , 'cache', TRUE);
}
@@ -3,7 +3,7 @@ README.txt
==========
Drupal module: i18n_sync (Synchronization)
This module will handle content synchronization accross translations.
This module will handle content synchronization across translations.
The available list of fields to synchronize will include standard node fields and cck fields.
To have aditional fields, add the list in a variable in the settings.php file, like this:
@@ -1,5 +1,5 @@
name = Synchronize translations
description = Synchronizes taxonomy and fields accross translations of the same content.
description = Synchronizes taxonomy and fields across translations of the same content.
dependencies[] = i18n
dependencies[] = translation
package = Multilingual - Internationalization
@@ -10,9 +10,8 @@ files[] = i18n_sync.install
files[] = i18n_sync.module.inc
files[] = i18n_sync.node.inc
files[] = i18n_sync.test
; Information added by drupal.org packaging script on 2013-01-13
version = "7.x-1.8"
; Information added by Drupal.org packaging script on 2020-06-17
version = "7.x-1.27"
core = "7.x"
project = "i18n"
datestamp = "1358075001"
datestamp = "1592373390"
@@ -11,8 +11,6 @@
*
* Notes:
* This module needs to run after taxonomy, i18n, translation. Check module weight.
*
* @ TODO Test with CCK when possible, api may have changed.
*/
/**
@@ -35,7 +33,7 @@ function i18n_sync($status = NULL) {
function i18n_sync_help($path, $arg) {
switch ($path) {
case 'admin/help#i18n_sync' :
$output = '<p>' . t('This module synchronizes content taxonomy and fields accross translations:') . '</p>';
$output = '<p>' . t('This module synchronizes content taxonomy and fields across translations:') . '</p>';
$output .= '<p>' . t('First you need to select which fields should be synchronized. Then, after a node has been updated, all enabled vocabularies and fields will be synchronized as follows:') . '</p>';
$output .= '<ul>';
$output .= '<li>' . t('All the node fields selected for synchronization will be set to the same value for all translations.') . '</li>';
@@ -71,25 +69,6 @@ function i18n_sync_field_info_alter(&$fields) {
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function i18n_sync_form_node_admin_content_alter(&$form, &$form_state) {
if (!empty($form['operation']) && $form['operation']['#value'] == 'delete') {
$form['#submit'] = array_merge(array('i18n_sync_node_delete_submit'), $form['#submit']);
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function i18n_sync_form_node_delete_confirm_alter(&$form, &$form_state) {
// Intercept form submission so we can handle uploads, replace callback
$form['#submit'] = array_merge(array('i18n_sync_node_delete_submit'), $form['#submit']);
}
/**
* Implements hook_form_FORM_ID_alter().
*/
@@ -112,6 +91,7 @@ function i18n_sync_form_node_type_form_alter(&$form, &$form_state) {
$form['i18n_sync']['i18n_sync_node_type'] = array(
'#tree' => TRUE,
'#weight' => 1,
);
// Each set provides title and options. We build a big checkboxes control for it to be
@@ -142,42 +122,21 @@ function i18n_sync_form_node_type_form_alter(&$form, &$form_state) {
);
}
}
}
}
/**
* Submit callback for
* - node delete confirm
* - node multiple delete confirm
*/
function i18n_sync_node_delete_submit($form, $form_state) {
if ($form_state['values']['confirm']) {
if (!empty($form_state['values']['nid'])) {
// Single node
i18n_sync_node_delete_prepare($form_state['values']['nid']);
}
elseif (!empty($form_state['values']['nodes'])) {
// Multiple nodes
foreach ($form_state['values']['nodes'] as $nid => $value) {
i18n_sync_node_delete_prepare($nid);
}
}
}
// Then it will go through normal form submission
}
/**
* Prepare node for deletion, work out synchronization issues
*/
function i18n_sync_node_delete_prepare($nid) {
$node = node_load($nid);
// Delete file associations when files are shared with existing translations
// so they are not removed by upload module
if (!empty($node->tnid) && module_exists('upload')) {
$result = db_query('SELECT u.* FROM {upload} u WHERE u.nid = %d AND u.fid IN (SELECT t.fid FROM {upload} t WHERE t.fid = u.fid AND t.nid <> u.nid)', $nid);
while ($up = db_fetch_object($result)) {
db_query("DELETE FROM {upload} WHERE fid = %d AND vid = %d", $up->fid, $up->vid);
}
// Add option to restrict syncing only when editing the translation source.
$form['i18n_sync']['i18n_sync_source_description'] = array(
'#prefix' => '<div>', '#suffix' => '</div>',
'#markup' => t('Restrict synchronization to the translation source.'),
'#weight' => 2,
);
$form['i18n_sync']['i18n_sync_source'] = array(
'#type' => 'checkbox',
'#title' => t('Synchronize translations only when saving the translation source'),
'#default_value' => variable_get('i18n_sync_source_' . $type, FALSE),
'#disabled' => $disabled,
'#weight' => 3,
'#description' => t('If not checked each node will trigger the synchronization, whether it\'s the source or not.'),
);
}
}
@@ -224,9 +183,17 @@ function i18n_sync_node_insert($node) {
*/
function i18n_sync_node_update($node) {
// Let's go with field synchronization.
if (i18n_sync_node_check($node) && !empty($node->tnid) && ($fields = i18n_sync_node_fields($node->type)) && ($translations = i18n_sync_node_get_translations($node, TRUE))) {
module_load_include('node.inc', 'i18n_sync');
i18n_sync_node_translation($node, $translations, $fields, 'update');
if (i18n_sync_node_check($node) && !empty($node->tnid) && (!variable_get('i18n_sync_source_' . $node->type, TRUE) || $node->tnid == $node->nid) && ($fields = i18n_sync_node_fields($node->type)) && ($translations = i18n_sync_node_get_translations($node, TRUE))) {
$do_sync = TRUE;
if (module_exists('entity_translation')) {
if (entity_translation_enabled_bundle('node', $node->type)) {
$do_sync = FALSE;
}
}
if ($do_sync) {
module_load_include('node.inc', 'i18n_sync');
i18n_sync_node_translation($node, $translations, $fields, 'update');
}
}
}
@@ -27,6 +27,7 @@
* Node operation (insert|update).
*/
function i18n_sync_node_translation($node, $translations, $field_names, $op) {
$total = count($translations);
$count = 0;
// Disable language selection and synchronization temporarily, enable it again later
$i18n_select = i18n_select(FALSE);
@@ -34,10 +35,12 @@ function i18n_sync_node_translation($node, $translations, $field_names, $op) {
foreach ($translations as $translation) {
// If translation is the same node, we cannot synchronize with itself
if ($node->nid == $translation->nid) {
$total--;
continue;
}
// Load full node, we need all data here.
$translation = node_load($translation->nid, NULL, TRUE);
entity_get_controller('node')->resetCache(array($translation->nid));
$translation = node_load($translation->nid);
$i18n_options = i18n_sync_node_options($node->type);
// Invoke callback for each field, the default is just copy over
foreach ($field_names as $field) {
@@ -53,6 +56,13 @@ function i18n_sync_node_translation($node, $translations, $field_names, $op) {
module_invoke_all('i18n_sync_translation', 'node', $translation, $translation->language, $node, $node->language, $field_names);
node_save($translation);
$count++;
// Flush each entity from the load cache after processing, to
// avoid exceeding PHP memory limits. It should be safe to keep
// at least one, however; so we retain the final translation in
// the cache after saving it.
if ($count < $total) {
entity_get_controller('node')->resetCache(array($translation->nid));
}
}
i18n_sync(TRUE);
i18n_select($i18n_select);
@@ -88,7 +98,7 @@ function i18n_sync_node_translation_nodereference_field(&$node, &$translation, $
* Example:
* English A references English B and English C.
* English A and B are translated to German A and B, but English C is not.
* The syncronization from English A to German A would it German B and English C.
* The synchronization from English A to German A would it German B and English C.
*/
function i18n_sync_node_translation_reference_field(&$reference_node, $default_value, $langcode) {
if (isset($reference_node->tnid) && translation_supported_type($reference_node->type)) {
@@ -0,0 +1,103 @@
CONTENTS OF THIS FILE
---------------------
* Introduction
* Requirements
* Recommended modules
* Installation
* Configuration
* Maintainers
INTRODUCTION
------------
The Taxonomy translation module, part of the Internationalization
(https://www.drupal.org/project/i18n) package, provides multiple options to
translate taxonomy vocabularies and terms. For each vocabulary, there are four
types of behaviors to choose from: Language-independent terms, Language-specific
terms, Localized terms, and Mixed-language vocabulary.
* For a full description of the module visit:
https://www.drupal.org/node/1114016
* To submit bug reports and feature suggestions, or to track changes visit:
https://www.drupal.org/project/issues/i18n
REQUIREMENTS
------------
This module requires the following module:
* Internationalization - https://www.drupal.org/project/i18n
RECOMMENDED MODULES
-------------------
* Internationalization Views - https://www.drupal.org/project/i18nviews
* Language Icons - https://www.drupal.org/project/languageicons
* Translation Overview - https://www.drupal.org/project/translation_overview
* Localization Client - https://www.drupal.org/project/l10n_client
* Internationalization contributions -
https://www.drupal.org/project/i18n_contrib
INSTALLATION
------------
This is a submodule of the Internationalization module. Install the
Internationalization module as you would normally install a contributed Drupal
module. Visit https://www.drupal.org/node/895232 for further information.
CONFIGURATION
-------------
Language-independent terms - only vocabulary will be translatable.
1. Navigate to Structure > Taxonomy.
2. Select the "edit vocabulary" link.
3. Select the "No multilingual options for terms".
Language-specific terms - vocabulary is only used for content in that language.
The terms will only be available if the term language matches the UI language.
1. Navigate to Structure > Taxonomy and select the "edit vocabulary link".
2. Choose "Fixed Language" and a Language drop-down field will be displayed.
3. Select the language.
4. Select "Fixed Language" and Save.
Localized terms - Terms are common for all languages, but their name and
description may be localized.
1. Navigate to Structure > Taxonomy > vocabulary-to-edit > Edit.
2. Select "Localize" and select Save.
3. Edit a term and there will be a Translate tab. Select this tab.
4. Select Translate, translate the Name and Description, select
"Save translation", and repeat for all languages.
5. Repeat the process for all terms.
6. Navigate to Structure > Content types > term-to-edit > Manage display.
7. By default, the term reference is set to Link. Change this to "Link
(localized)" and Save.
The vocabulary will be appropriate for the language.
Mixed-language vocabulary - Use for vocabularies with terms in multiple
languages.
1. Navigate to Structure > Taxonomy > vocabulary-to-edit > Edit.
2. Select the Translate radio button and Save.
3. Edit a vocabulary term and there will be a new Language field. Choose a
language and then select Save and translate.
4. There are two options, the user can either select "Add translation link" or
the user can select an existing term in the Select translations form.
5. Create translations for the terms and add terms for specific languages only.
Now if the user edits a node associated with this vocabulary, only the relevant
terms will appear.
MAINTAINERS
-----------
* Jose Reyero - https://www.drupal.org/u/jose-reyero
* Florian Weber (webflo) - https://www.drupal.org/u/webflo
* Peter Philipp - https://www.drupal.org/u/das-peter
* Joseph Olstad - https://www.drupal.org/u/joseph.olstad
* Nathaniel Catchpole - https://www.drupal.org/u/catch
@@ -48,7 +48,7 @@ function i18n_taxonomy_translation_term_form($form, $form_state, $vocabulary, $t
$form['translations'][$lang] = array(
'#title' => $langname,
'#type' => 'item',
'#markup' => $source->name,
'#markup' => check_plain($source->name),
'#langcode' => $lang,
);
}
@@ -199,7 +199,14 @@ function i18n_taxonomy_translation_term_overview($term) {
$path = 'taxonomy/term/' . $translation_term->tid;
$title = l($translation_term->name, $path);
$options[] = l(t('edit'), $path . '/edit');
$options['edit'] = array(
'#type' => 'link',
'#title' => t('edit'),
'#href' => $path . '/edit',
'#options' => array(
'query' => drupal_get_destination(),
),
);
if ($translation_term->tid == $i18n_tsid) {
$language_name = t('<strong>@language_name</strong> (source)', array('@language_name' => $language_name));
@@ -208,14 +215,25 @@ function i18n_taxonomy_translation_term_overview($term) {
else {
// No such translation in the set yet: help user to create it.
$title = t('n/a');
$options[] = l(t('add translation'), 'admin/structure/taxonomy/' . $term->vocabulary_machine_name . '/add', array('query' => array('translation' => $term->tid, 'target' => $langcode) + drupal_get_destination()));
$options['add'] = array(
'#type' => 'link',
'#title' => t('add translation'),
'#href' => 'admin/structure/taxonomy/' . $term->vocabulary_machine_name . '/add',
'#options' => array(
'query' => array('translation' => $term->tid, 'target' => $langcode) + drupal_get_destination()
),
);
}
$rows[] = array($language_name, $title, implode(" | ", $options));
$rows[$langcode] = array(
'language' => $language_name,
'title' => $title,
'operations' => array('data' => $options),
);
}
drupal_set_title(t('Translations of term %title', array('%title' => $term->name)), PASS_THROUGH);
$build['translation_node_overview'] = array(
$build['translation_overview'] = array(
'#theme' => 'table',
'#header' => $header,
'#rows' => $rows,
@@ -11,9 +11,8 @@ files[] = i18n_taxonomy.pages.inc
files[] = i18n_taxonomy.admin.inc
files[] = i18n_taxonomy.test
; Information added by drupal.org packaging script on 2013-01-13
version = "7.x-1.8"
; Information added by Drupal.org packaging script on 2020-06-17
version = "7.x-1.27"
core = "7.x"
project = "i18n"
datestamp = "1358075001"
datestamp = "1592373390"
@@ -12,7 +12,7 @@
*/
function i18n_taxonomy_install() {
module_load_install('i18n');
i18n_install_create_fields('taxonomy_vocabulary', array('language', 'i18n_mode'));
i18n_install_create_fields('taxonomy_vocabulary', array('language', 'i18n_mode'), TRUE);
i18n_install_create_fields('taxonomy_term_data', array('language', 'i18n_tsid'));
// Set module weight for it to run after core modules, but before views.
db_query("UPDATE {system} SET weight = 5 WHERE name = 'i18n_taxonomy' AND type = 'module'");
@@ -184,11 +184,11 @@ function i18n_taxonomy_field_formatter_info() {
}
/**
- * Implements hook_field_formatter_prepare_view().
- *
- * This preloads all taxonomy terms for multiple loaded objects at once and
- * unsets values for invalid terms that do not exist.
- */
* Implements hook_field_formatter_prepare_view().
*
* This preloads all taxonomy terms for multiple loaded objects at once and
* unsets values for invalid terms that do not exist.
*/
function i18n_taxonomy_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
return taxonomy_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, $items, $displays);
}
@@ -213,7 +213,12 @@ function i18n_taxonomy_field_formatter_view($entity_type, $entity, $field, $inst
);
}
else {
$term = $item['taxonomy_term'];
if (isset($item['taxonomy_term'])) {
$term = $item['taxonomy_term'];
}
else {
$term = taxonomy_term_load($item['tid']);
}
$uri = entity_uri('taxonomy_term', $term);
$element[$delta] = array(
'#type' => 'link',
@@ -318,7 +323,7 @@ function i18n_taxonomy_field_storage_details_alter(&$details, &$field) {
/**
* Implements hook_field_attach_prepare_translation_alter().
*
* Prepare and synchronize translation for term reference fields.
*/
function i18n_taxonomy_field_attach_prepare_translation_alter(&$entity, $context) {
@@ -372,10 +377,19 @@ function i18n_taxonomy_field_prepare_translation($entity_type, $entity, $field,
* The array of valid terms for this field, keyed by term id.
*/
function i18n_taxonomy_allowed_values($field) {
global $language;
$options = array();
foreach ($field['settings']['allowed_values'] as $tree) {
if ($vocabulary = taxonomy_vocabulary_machine_name_load($tree['vocabulary'])) {
if ($terms = taxonomy_get_tree($vocabulary->vid, $tree['parent'])) {
if (i18n_taxonomy_vocabulary_mode($vocabulary->vid) == I18N_MODE_TRANSLATE) {
$parent = i18n_taxonomy_translation_term_tid($tree['parent'], NULL, $tree['parent']);
$language = i18n_language_context();
$terms = i18n_taxonomy_get_tree($vocabulary->vid, $language->language, $parent);
}
else {
$terms = taxonomy_get_tree($vocabulary->vid, $tree['parent']);
}
if ($terms) {
foreach ($terms as $term) {
$options[$term->tid] = str_repeat('-', $term->depth) . i18n_taxonomy_term_name($term);
}
@@ -452,23 +466,13 @@ function i18n_taxonomy_translate_path($path, $path_prefix = 'taxonomy/term/') {
return $links;
}
}
/**
* Implements hook_theme().
*/
function i18n_taxonomy_theme() {
return array(
'i18n_taxonomy_term_page' => array(
'arguments' => array('tids' => array(), 'result' => NULL),
'file' => 'i18n_taxonomy.pages.inc',
),
);
}
/**
* Get localized term name unfiltered.
*/
function i18n_taxonomy_term_name($term, $langcode = NULL) {
return i18n_taxonomy_vocabulary_mode($term->vid, I18N_MODE_LOCALIZE) ? i18n_string(array('taxonomy', 'term', $term->tid, 'name'), $term->name, array('langcode' => $langcode, 'sanitize' => FALSE)) : $term->name;
$key = i18n_object_info('taxonomy_term', 'key');
return i18n_taxonomy_vocabulary_mode($term->vid, I18N_MODE_LOCALIZE) ? i18n_string(array('taxonomy', 'term', $term->{$key}, 'name'), $term->name, array('langcode' => $langcode, 'sanitize' => FALSE)) : $term->name;
}
@@ -476,7 +480,8 @@ function i18n_taxonomy_term_name($term, $langcode = NULL) {
* Get localized term description unfiltered.
*/
function i18n_taxonomy_term_description($term, $langcode = NULL) {
return i18n_taxonomy_vocabulary_mode($term->vid, I18N_MODE_LOCALIZE) ? i18n_string(array('taxonomy', 'term', $term->tid, 'description'), $term->description, array('langcode' => $langcode, 'sanitize' => FALSE)) : $term->description;
$key = i18n_object_info('taxonomy_term', 'key');
return i18n_taxonomy_vocabulary_mode($term->vid, I18N_MODE_LOCALIZE) ? i18n_string(array('taxonomy', 'term', $term->{$key}, 'description'), $term->description, array('langcode' => $langcode, 'sanitize' => FALSE)) : $term->description;
}
/**
@@ -751,6 +756,28 @@ function i18n_taxonomy_form_taxonomy_form_term_alter(&$form, &$form_state) {
// Add language field or not depending on taxonomy mode.
switch (i18n_taxonomy_vocabulary_mode($vocabulary->vid)) {
case I18N_MODE_TRANSLATE:
// Set $form_state['storage'] default as empty array because we will add
// the translation and target from $_GET. So we still have it when the
// page partially reloads with ajax.
if(!isset($form_state['storage'])) {
$form_state['storage'] = array();
}
// get translation from $_GET or $form_state['storage']
$translation = null;
if(isset($_GET['translation'])) {
$translation = $_GET['translation'];
$form_state['storage']['translation'] = $translation;
} else if(isset($form_state['storage']) && isset($form_state['storage']['translation'])){
$translation = $form_state['storage']['translation'];
}
// get target from $_GET or $form_state['storage']
$target = null;
if(isset($_GET['target'])) {
$target = $_GET['target'];
$form_state['storage']['target'] = $target;
} else if(isset($form_state['storage']) && isset($form_state['storage']['target'])){
$target = $form_state['storage']['target'];
}
$form['language'] = array(
'#description' => t('This term belongs to a multilingual vocabulary. You can set a language for it.'),
) + i18n_element_language_select($term);
@@ -758,7 +785,7 @@ function i18n_taxonomy_form_taxonomy_form_term_alter(&$form, &$form_state) {
// If the term to be added will be a translation of a source term,
// set the default value of the option list to the target language and
// create a form element for storing the translation set of the source term.
if (empty($term->tid) && isset($_GET['translation']) && isset($_GET['target']) && ($source_term = taxonomy_term_load($_GET['translation'])) && ($target_language = i18n_language_object($_GET['target']))) {
if (empty($term->tid) && isset($translation) && isset($target) && ($source_term = taxonomy_term_load($translation)) && ($target_language = i18n_language_object($target))) {
// Set context language to target language.
i18n_language_context($target_language);
@@ -1000,7 +1027,7 @@ function i18n_taxonomy_translate_terms($taxonomy, $langcode, $fullterms = TRUE)
}
elseif ($term->language && $term->language != $langcode) {
$translation_set = i18n_translation_set_load($term->i18n_tsid);
$translations = $translation_set->get_translations();
$translations = ($translation_set) ? $translation_set->get_translations() : NULL;
if ($translations && !empty($translations[$langcode])) {
$newterm = $translations[$langcode];
@@ -1029,6 +1056,10 @@ function i18n_taxonomy_localize_terms($terms) {
if (!i18n_string_translate_langcode()) {
return $terms;
}
// $terms is not a valid array or term.
if (empty($terms)) {
return $terms;
}
$object_info = i18n_object_info('taxonomy_term');
$list = is_array($terms) ? $terms : array($terms);
foreach ($list as $index => $term) {
@@ -1096,7 +1127,7 @@ function i18n_taxonomy_get_tree($vid, $langcode, $parent = 0, $max_depth = NULL,
$query->join('taxonomy_term_hierarchy', 'h', 'h.tid = t.tid');
$result = $query
->addTag('translatable')
->addTag('term_access')
->addTag('taxonomy_term_access')
->fields('t')
->fields('h', array('parent'))
->condition('t.vid', $vid)
@@ -1225,10 +1256,18 @@ function i18n_taxonomy_field_uuid_presave($entity_type, $entity, $field, $instan
* Implements hook_entity_info_alter().
*/
function i18n_taxonomy_entity_info_alter(&$entity_info) {
if (isset($entity_info['taxonomy_vocabulary'])) {
// Add altered vocabulary schema fields.
$entity_info['taxonomy_vocabulary']['schema_fields_sql']['base table'][] = 'i18n_mode';
$entity_info['taxonomy_vocabulary']['schema_fields_sql']['base table'][] = 'language';
}
if (isset($entity_info['taxonomy_term'])) {
// Core doesn't provide a label callback for taxonomy terms. By setting one
// we can use it to return the correct localized term name.
$entity_info['taxonomy_term']['label callback'] = 'i18n_taxonomy_taxonomy_term_label';
// Also let core know terms have languages, now.
$entity_info['taxonomy_term']['entity keys']['language'] = 'language';
}
}
@@ -1260,10 +1299,29 @@ function i18n_taxonomy_modules_enabled($modules) {
$modules = drupal_map_assoc($modules);
if (isset($modules['i18n_taxonomy'])) {
foreach (field_info_fields() as $fieldname => $field) {
if ($field['type'] == 'taxonomy_term_reference') {
$field['settings']['options_list_callback'] = 'i18n_taxonomy_allowed_values';
field_update_field($field);
}
if ($field['type'] == 'taxonomy_term_reference') {
$field['settings']['options_list_callback'] = 'i18n_taxonomy_allowed_values';
field_update_field($field);
}
}
}
}
/**
* Implements hook_views_pre_render().
*/
function i18n_taxonomy_views_pre_render(&$view) {
if($view->base_table !== 'rules_scheduler') {
global $language;
foreach ($view->result as $delta => $term){
if (isset($term->tid)) {
i18n_string_translate_langcode($language->language);
$localized_term = i18n_taxonomy_localize_terms(taxonomy_term_load($term->tid));
$term->tid = $localized_term->tid;
$term->taxonomy_term_data_name = $localized_term->name;
$term->taxonomy_term_data_description = $localized_term->description;
}
}
}
}
@@ -62,37 +62,6 @@ function i18n_taxonomy_term_page($term) {
return $build;
}
/**
* Render a taxonomy term page HTML output.
*
* @param $tids
* An array of term ids.
* @param $result
* A pager_query() result, such as that performed by taxonomy_select_nodes().
*
* @ingroup themeable
*/
function theme_i18n_taxonomy_term_page($tids, $result) {
drupal_add_css(drupal_get_path('module', 'taxonomy') . '/taxonomy.css');
$output = '';
// Only display the description if we have a single term, to avoid clutter and confusion.
if (count($tids) == 1) {
$term = i18n_taxonomy_localize_terms(taxonomy_term_load($tids[0]));
// Check that a description is set.
if (!empty($term->description)) {
$output .= '<div class="taxonomy-term-description">';
$output .= filter_xss_admin($term->description);
$output .= '</div>';
}
}
$output .= taxonomy_render_nodes($result);
return $output;
}
/**
* Helper function for autocompletion. Replacement for taxonomy_autocomplete
*/
@@ -131,7 +100,7 @@ function _i18n_taxonomy_autocomplete($langcode, $vids, $tags_typed = '') {
$query = db_select('taxonomy_term_data', 't')
->fields('t', array('tid', 'name'));
$query->addTag('translatable');
$query->addTag('term_access');
$query->addTag('taxonomy_term_access');
// Disable i18n_select for this query
$query->addTag('i18n_select');
// Add language condition
@@ -15,6 +15,10 @@ function i18n_taxonomy_token_info() {
'name' => t("Name (localized)"),
'description' => t("The name of the taxonomy term."),
);
$term['localized-name'] = array(
'name' => t("Name in current language"),
'description' => t("The name of the taxonomy term in current language."),
);
$term['i18n-description'] = array(
'name' => t("Description (localized)"),
'description' => t("The optional description of the taxonomy term."),
@@ -69,6 +73,12 @@ function i18n_taxonomy_tokens($type, $tokens, array $data = array(), array $opti
$replacements[$original] = $sanitize ? check_plain($name) : $name;
break;
case 'localized-name':
$translated_term = i18n_taxonomy_term_get_translation($term, $langcode);
$name = i18n_taxonomy_term_name($translated_term, $langcode);
$replacements[$original] = $sanitize ? check_plain($name) : $name;
break;
case 'i18n-description':
$replacements[$original] = i18n_string_text(array('taxonomy', 'term', $term->tid, 'description'), $term->description, array('langcode' => $langcode, 'format' => $term->format, 'sanitize' => $sanitize, 'cache' => TRUE));
break;
@@ -6,9 +6,8 @@ core = 7.x
files[] = i18n_translation.inc
; Information added by drupal.org packaging script on 2013-01-13
version = "7.x-1.8"
; Information added by Drupal.org packaging script on 2020-06-17
version = "7.x-1.27"
core = "7.x"
project = "i18n"
datestamp = "1358075001"
datestamp = "1592373390"
@@ -20,7 +20,7 @@ class I18nTranslationSetController extends DrupalDefaultEntityController {
* @param $queried_entities
* Associative array of query results, keyed on the entity ID.
* @param $revision_id
* ID of the revision that was loaded, or FALSE if teh most current revision
* ID of the revision that was loaded, or FALSE if the most current revision
* was loaded.
*/
protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
@@ -0,0 +1,70 @@
CONTENTS OF THIS FILE
---------------------
* Introduction
* Requirements
* Recommended modules
* Installation
* Configuration
* Maintainers
INTRODUCTION
------------
The User mail translation module, part of the Internationalization
(https://www.drupal.org/project/i18n) package, translates emails sent from the
User module.
* For a full description of the module, visit this page:
https://www.drupal.org/node/133977.
* To submit bug reports and feature suggestions, or to track changes:
https://www.drupal.org/project/issues/i18n.
REQUIREMENTS
------------
This module requires the following module:
* Internationalization - https://www.drupal.org/project/i18n
RECOMMENDED MODULES
-------------------
* Internationalization Views - https://www.drupal.org/project/i18nviews
* Language Icons - https://www.drupal.org/project/languageicons
* Translation Overview - https://www.drupal.org/project/translation_overview
* Localization Client - https://www.drupal.org/project/l10n_client
* Internationalization contributions -
https://www.drupal.org/project/i18n_contrib
INSTALLATION
------------
This is a submodule of the Internationalization module. Install the
Internationalization module as you would normally install a contributed Drupal
module. Visit https://www.drupal.org/node/895232 for further information.
CONFIGURATION
-------------
To configure email translations
1. Navigate to Administration > Configuration > Regional and language >
Multilingual settings and select the variables tab.
2. Select the "User emails" tab. Select the variables to be translated. Save
configuration.
MAINTAINERS
-----------
* Jose Reyero - https://www.drupal.org/u/jose-reyero
* Florian Weber (webflo) - https://www.drupal.org/u/webflo
* Peter Philipp - https://www.drupal.org/u/das-peter
* Joseph Olstad - https://www.drupal.org/u/joseph.olstad
* Nathaniel Catchpole - https://www.drupal.org/u/catch
@@ -4,9 +4,8 @@ core = 7.x
package = Multilingual - Internationalization
dependencies[] = i18n_variable
; Information added by drupal.org packaging script on 2013-01-13
version = "7.x-1.8"
; Information added by Drupal.org packaging script on 2020-06-17
version = "7.x-1.27"
core = "7.x"
project = "i18n"
datestamp = "1358075001"
datestamp = "1592373390"
@@ -9,7 +9,14 @@
*/
function i18n_user_mail_alter(&$message) {
if ($message['module'] == 'user') {
$language = $message['language'];
$message['language'] = language_default();
if (isset($message['params']['account'])) {
$user_preferred = user_preferred_language($message['params']['account']);
if (isset($user_preferred)) {
$message['language'] = $user_preferred;
}
}
$language = (isset($message['language']) ? $message['language'] : language_default());
$variables = array('user' => $message['params']['account']);
$key = $message['key'];
@@ -54,7 +61,7 @@ function i18n_user_user_mail_tokens(&$replacements, $data, $options) {
*/
function i18n_user_user_pass_reset_url($account) {
$timestamp = REQUEST_TIME;
return url("user/reset/$account->uid/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login), array('absolute' => TRUE, 'language' => i18n_language($account->language)));
return url("user/reset/$account->uid/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid), array('absolute' => TRUE, 'language' => i18n_language($account->language)));
}
/**
@@ -65,5 +72,5 @@ function i18n_user_user_pass_reset_url($account) {
*/
function i18n_user_user_cancel_url($account) {
$timestamp = REQUEST_TIME;
return url("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login), array('absolute' => TRUE, 'language' => i18n_language($account->language)));
return url("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid), array('absolute' => TRUE, 'language' => i18n_language($account->language)));
}
@@ -0,0 +1,84 @@
CONTENTS OF THIS FILE
---------------------
* Introduction
* Requirements
* Recommended modules
* Installation
* Configuration
* Maintainers
INTRODUCTION
------------
The Variable translation module, part of the Internationalization
(https://www.drupal.org/project/i18n) package, allows the user to translate text
and settings that are stored in Drupal as variables. These variables include
text such as 'site name' and 'site slogan', as well as settings like 'Default
front page' and 'Default 404 page'.
* For a full description of the module, visit:
https://www.drupal.org/node/1113374.
* To submit bug reports and feature suggestions, or to track changes visit:
https://www.drupal.org/project/issues/i18n.
REQUIREMENTS
------------
This module requires the following modules:
* Internationalization - https://www.drupal.org/project/i18n
* Variable - https://www.drupal.org/project/variable
RECOMMENDED MODULES
-------------------
* Internationalization Views - https://www.drupal.org/project/i18nviews
* Language Icons - https://www.drupal.org/project/languageicons
* Translation Overview - https://www.drupal.org/project/translation_overview
* Localization Client - https://www.drupal.org/project/l10n_client
* Internationalization contributions -
https://www.drupal.org/project/i18n_contrib
INSTALLATION
------------
This is a submodule of the Internationalization module. Install the
Internationalization module as you would normally install a contributed Drupal
module. Visit https://www.drupal.org/node/895232 for further information.
CONFIGURATION
-------------
To enable multilingual variables
1. Enable the Variable translation module included with the Internationalization
package.
2. Go to Administration > Configuration > Regional and language > Multilingual
settings.
3. Select on the Variables tab.
4. Select the variables that will be multilingual.
5. Save configuration.
Once the user has the correct settings, they'll be marked with "This is a
multilingual variable" when the user navigates to the corresponding
administration pages. The user must switch the site language while in the
administration pages to set the variables for each language. A language switcher
link will appear at the top of each administrative page that has multilingual
variables.
MAINTAINERS
-----------
* Jose Reyero - https://www.drupal.org/u/jose-reyero
* Florian Weber (webflo) - https://www.drupal.org/u/webflo
* Peter Philipp - https://www.drupal.org/u/das-peter
* Joseph Olstad - https://www.drupal.org/u/joseph.olstad
* Nathaniel Catchpole - https://www.drupal.org/u/catch
@@ -10,9 +10,8 @@ configure = admin/config/regional/i18n/variable
files[] = i18n_variable.class.inc
files[] = i18n_variable.test
; Information added by drupal.org packaging script on 2013-01-13
version = "7.x-1.8"
; Information added by Drupal.org packaging script on 2020-06-17
version = "7.x-1.27"
core = "7.x"
project = "i18n"
datestamp = "1358075001"
datestamp = "1592373390"
@@ -7,9 +7,8 @@ package = Testing
core = 6.x
hidden = TRUE
; Information added by drupal.org packaging script on 2013-01-13
version = "7.x-1.8"
; Information added by Drupal.org packaging script on 2020-06-17
version = "7.x-1.27"
core = "7.x"
project = "i18n"
datestamp = "1358075001"
datestamp = "1592373390"
@@ -36,6 +36,12 @@ function i18n_test_i18n_string_info() {
'format' => FALSE, // This group doesn't have strings with format
'refresh callback' => 'i18n_test_i18n_string_refresh',
);
$groups['test_cached'] = array(
'title' => t('Test Cached Strings'),
'description' => t('Translatable items of a textgroup with caching enabled.'),
'format' => FALSE, // This group doesn't have strings with format
'class' => variable_get('i18n_string_textgroup_class_test_cached', 'i18n_string_textgroup_cached_logged'),
);
return $groups;
}
/**
@@ -43,4 +49,53 @@ function i18n_test_i18n_string_info() {
*/
function i18n_test_i18n_string_refresh() {
return TRUE;
}
/**
* Implements hook_menu().
*/
function i18n_test_menu() {
// Required for the i18n_string caching tests.
$items['tests/i18n/i18n_string_build/%'] = array(
'title' => 'Load string',
'access callback' => TRUE,
'page callback' => 'i18n_string_build',
'page arguments' => array(3),
'type' => MENU_CALLBACK,
'delivery callback' => 'drupal_json_output',
);
$items['tests/i18n/i18n_string_build/%/%'] = array(
'title' => 'Load string',
'access callback' => TRUE,
'page callback' => 'i18n_string_build',
'page arguments' => array(3, 4),
'type' => MENU_CALLBACK,
'delivery callback' => 'drupal_json_output',
);
$items['tests/i18n/i18n_string_translation_search/%'] = array(
'title' => 'Search string translations',
'access callback' => TRUE,
'page callback' => 'i18n_string_translation_search',
'page arguments' => array(3),
'type' => MENU_CALLBACK,
'delivery callback' => 'drupal_json_output',
);
$items['tests/i18n/i18n_string_translation_search/%/%'] = array(
'title' => 'Search string translations',
'access callback' => TRUE,
'page callback' => 'i18n_string_translation_search',
'page arguments' => array(3, 4),
'type' => MENU_CALLBACK,
'delivery callback' => 'drupal_json_output',
);
return $items;
}
class i18n_string_textgroup_cached_logged extends i18n_string_textgroup_cached {
public static function load_translation($i18nstring, $langcode) {
$strings = variable_get('i18n_loaded_translations', array());
$strings[$i18nstring->get_name()] = true;
variable_set('i18n_loaded_translations', $strings);
parent::load_translation($i18nstring, $langcode);
}
}
@@ -26,42 +26,43 @@ Installation
Download, unpack the module the usual way.
Enable this module and the Locale module (core).
You need at least one language other than English.
On Administration > Configuration > Regional and language:
Click "Add language"
Pull-down menu: Choose your new language
Then click "Add language"
You need at least one language (besides the default English).
On Administration > Configuration > Regional and language > Languages:
Click "Add language".
Select a language from the select list "Language name".
Then click the "Add language" button.
Drupal is now importing interface translations. This can take a few minutes.
When it's finished, you'll get a confirmation with a summary of all
translation files that have been pulled in.
When it's finished, you'll get a confirmation with a summary of the
translations that have been imported.
If required, enable the new language as default language.
Home > Administration > Configuration > Regional and language:
Select your new language as default
Administration > Configuration > Regional and language > Languages:
Select your new default language.
Update interface translations
-----------------------------
On Home > Administration > Configuration > Regional and language:
Choose the "Translation updates" tab
Change "Check for updates" to Daily or Weekly instead of the default "Never".
You want to import translations regularly using cron. You can enable this
on Administration > Configuration > Regional and language > Languages:
Choose the "Translation updates" tab.
Change "Check for updates" to "Daily" or "Weekly" instead of the default "Never".
From now on cron will check for updated translations, and import them is required.
Cron will from now on check for updated translations, and will report the
update status on the status page (Home > Administration > Reports).
The status of the translations is reported on the "Status report" page at
Administration > Reports.
To check the translation status and execute updates manually, go to
Administration > Configuration > Regional and language > Translate inteface
Here you see English and your new language.
Choose the "Update" tab
Administration > Configuration > Regional and language > Translate inteface
Choose the "Update" tab.
You see a list of all modules and their translation status.
On the bottom of the page, you can manually update using "Update translations".
Use Drush
---------
You can also use drush to update your translations:
drush l10n-update # Update translations.
drush l10n-update-refresh # Refresh available information.
drush l10n-update-status # Show translation status of available project
drush l10n-update # Update translations.
drush l10n-update-refresh # Refresh available information.
drush l10n-update-status # Show translation status of available project
Summary of administrative pages
@@ -59,7 +59,7 @@ function l10n_update_admin_overview() {
$build['admin_import_form'] = drupal_get_form('l10n_update_admin_import_form', $projects, $updates);
}
else {
$build['no_projects'] = array('#markup' => t('No projects or languages to update.'));
$build['no_languages'] = array('#markup' => t('No translatable language defined. <a href="/admin/config/regional/language">Add a language</a>.'));
}
return $build;
}
@@ -96,30 +96,29 @@ function l10n_update_admin_import_form($form, $form_state, $projects, $updates)
'#type' => 'value',
'#value' => $updates,
);
// @todo Only show this language fieldset if we have more than 1 language.
$form['lang'] = array(
'#type' => 'fieldset',
'#title' => t('Languages'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#description' => t('Select one or more languages to download and update. If you select none, all of them will be updated.'),
);
$form['lang']['languages'] = array(
'#type' => 'checkboxes',
'#options' => $languages,
);
$form['mode'] = array(
'#type' => 'radios',
'#title' => t('Update mode'),
'#default_value' => variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP),
'#options' => _l10n_update_admin_import_options(),
);
$form['buttons']['download'] = array(
'#type' => 'submit',
'#value' => t('Update translations'),
);
if (count($languages) > 1) {
$form['lang'] = array(
'#type' => 'fieldset',
'#title' => t('Languages'),
'#collapsible' => TRUE,
'#collapsed' => FALSE ,
'#description' => t('Select one or more languages to download and update. If you select none, all of them will be updated.'),
);
$form['lang']['languages'] = array(
'#type' => 'checkboxes',
'#options' => $languages,
);
}
if ($updates) {
$form['actions']['download'] = array(
'#type' => 'submit',
'#value' => t('Update translations'),
);
}
}
$form['buttons']['refresh'] = array(
$form['actions']['refresh'] = array(
'#type' => 'submit',
'#value' => t('Refresh information'),
);
@@ -137,9 +136,10 @@ function l10n_update_admin_import_form_submit($form, $form_state) {
$projects = l10n_update_get_projects();
if ($op == t('Update translations')) {
$languages = array_filter($form_state['values']['languages']);
$languages = isset($form_state['values']['languages']) ? array_filter($form_state['values']['languages']) : NULL;
$updates = $form_state['values']['updates'];
$mode = $form_state['values']['mode'];
$mode = variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP);
if ($projects && $updates) {
module_load_include('batch.inc', 'l10n_update');
// Filter out updates in other languages. If no languages, all of them will be updated
@@ -147,9 +147,6 @@ function l10n_update_admin_import_form_submit($form, $form_state) {
$batch = l10n_update_batch_multiple($updates, $mode);
batch_set($batch);
}
else {
drupal_set_message(t('Cannot find any translation updates.'), 'error');
}
}
elseif ($op == t('Refresh information')) {
// Get current version of projects.
@@ -205,7 +202,11 @@ function l10n_update_admin_settings_form($form, &$form_state) {
'#default_value' => variable_get('l10n_update_download_store', ''),
'#description' => t('A path relative to the Drupal installation directory where translation files will be stored, e.g. sites/all/translations. Saved translation files can be reused by other installations. If left empty the downloaded translation will not be saved.'),
);
return system_settings_form($form);
$form = system_settings_form($form);
$form['#submit'][] = 'l10n_update_admin_settings_form_submit';
return $form;
}
/**
@@ -223,6 +224,14 @@ function l10n_update_admin_settings_form_validate($form, &$form_state) {
}
}
/**
* Additional submit handler for update settings.
*/
function l10n_update_admin_settings_form_submit($form, &$form_state) {
// Add .htaccess file to the translations directory.
l10n_update_ensure_htaccess();
}
/**
* Get array of import options.
*
@@ -344,6 +353,7 @@ function theme_l10n_update_project_status($variables) {
// Plus add our own css and js.
drupal_add_css(drupal_get_path('module', 'update') . '/update.css');
drupal_add_css(drupal_get_path('module', 'l10n_update') . '/css/l10n_update.admin.css');
drupal_add_js('misc/collapse.js');
drupal_add_js(drupal_get_path('module', 'l10n_update') . '/js/l10n_update.js');
return $output;

Some files were not shown because too many files have changed in this diff Show More