diff --git a/sites/all/modules/i18n/INSTALL.txt b/sites/all/modules/i18n/INSTALL.txt
new file mode 100644
index 0000000..00cf049
--- /dev/null
+++ b/sites/all/modules/i18n/INSTALL.txt
@@ -0,0 +1,39 @@
+********************************************************************
+ D R U P A L M O D U L E
+********************************************************************
+Name: i18n module and translation module
+Author: Jose A. Reyero
+
+********************************************************************
+ This is the 6.x version of i18n module, and works with Drupal 6.x
+********************************************************************
+
+********************************************************************
+Updated documentation will be kept on-line at http://drupal.org/node/133977
+********************************************************************
+
+INSTALLATION:
+============
+
+1. Create folder 'sites/all/modules/i18n' and copy all the modules files, keeping directory structure, to this folder.
+2. If updating, run the update.php script following the standard procedure for Drupal updates.
+
+POST-INSTALLATION/CONFIGURATION:
+============
+- First of all review Drupal language settings and make sure you have chosen the right default language.
+- Enable the needed modules grouped under "Internationalization" package
+- Read the on-line handbook on
+
+IMPORTANT:
+==========
+- This module requires a complex set up, make sure you read the handbook and understand the different options
+- Before creating a support request, do read the handbook: http://drupal.org/node/133977
+
+Additional Support
+==================
+For support, please create a support request for this module's project: http://drupal.org/project/i18n
+
+Support questions by email to the module maintainer will be simply ignored. Use the issue tracker.
+
+====================================================================
+Jose A. Reyero, freelance at reyero dot net, http://www.reyero.net
diff --git a/sites/all/modules/i18n/LICENSE.txt b/sites/all/modules/i18n/LICENSE.txt
new file mode 100644
index 0000000..d159169
--- /dev/null
+++ b/sites/all/modules/i18n/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
'. t('This is the base module for several others adding different features:') .'
'; + $output .= ''. t('For more information, see the online handbook entry for Internationalization module.', array('@i18n' => 'http://drupal.org/node/133977')) .'
'; + return $output; + + case 'admin/settings/language/i18n': + $output = ''. t('This module provides support for multilingual blocks.') .'
'; + $output .= ''. t('You can set up a language for a block or define it as translatable:') .'
'; + $output .= ''. t('To search and translate strings, use the translation interface pages.', array('@translate-interface' => url('admin/build/translate'))) .'
'; + return $output; + } +} + +/** + * Implementation of hook_db_rewrite_sql(). + */ +function i18nblocks_db_rewrite_sql($query, $primary_table, $primary_key) { + if ($primary_table == 'b' && $primary_key == 'bid') { + $return['join'] = 'LEFT JOIN {i18n_blocks} i18n ON (b.module = i18n.module AND b.delta = i18n.delta)'; + $return['where'] = i18n_db_rewrite_where('i18n', 'block', 'simple'); + return $return; + } +} + +/** + * Implementation of hook_locale(). + * + * This one doesn't need locale refresh because strings are stored from module config form. + */ +function i18nblocks_locale($op = 'groups', $group = NULL) { + switch ($op) { + case 'groups': + return array('blocks' => t('Blocks')); + case 'info': + $info['blocks']['refresh callback'] = 'i18nblocks_locale_refresh'; + $info['blocks']['format'] = TRUE; + return $info; + } +} + +/** + * Refresh all strings. + */ +function i18nblocks_locale_refresh() { + $result = db_query("SELECT DISTINCT b.module, b.delta, b.title, bx.body, bx.format, i.ibid, i.language FROM {blocks} b LEFT JOIN {boxes} bx ON b.module = 'block' AND b.delta = bx.bid LEFT JOIN {i18n_blocks} i ON b.module = i.module AND b.delta = i.delta"); + while ($block = db_fetch_object($result)) { + if (!$block->language) { + // If the block has a custom title and no language it must be translated + if ($block->title && $block->title != ''. t('To search and translate strings, use the translation interface pages.', array('@translate-interface' => url('admin/build/translate'))) .'
'; + return $output; + } + // Add translated help text for node add pages + if (!menu_get_object() && $arg[1] == 'add' && $arg[2]) { + $type = str_replace('-', '_', $arg[2]); + // Fetch default language node type help + $source = i18ncontent_node_help_source($type); + if (isset($source->source) && ($help = i18nstrings("nodetype:type:$type:help", $source->source))) { + return ''. filter_xss_admin($help) .'
'; + } + } +} + +/** + * Implementation of hook_locale(). + */ +function i18ncontent_locale($op = 'groups', $group = NULL) { + switch ($op) { + case 'groups': + return array('nodetype' => t('Content type')); + case 'info': + $info['nodetype']['refresh callback'] = 'i18ncontent_locale_refresh'; + $info['nodetype']['format'] = FALSE; + return $info; + } +} + +/** + * Refresh content type strings. + */ +function i18ncontent_locale_refresh() { + foreach (node_get_types() as $type) { + i18nstrings_update("nodetype:type:$type->type:name", $type->name); + i18nstrings_update("nodetype:type:$type->type:title", $type->title_label); + if (isset($type->body_label)) { + i18nstrings_update("nodetype:type:$type->type:body", $type->body_label); + } + i18nstrings_update("nodetype:type:$type->type:description", $type->description); + if ($type->help) { + i18nstrings_ts("nodetype:type:$type->type:help", $type->help, NULL, TRUE); + $type->help = ''; + node_type_save($type); + } + } + return TRUE; // Meaning it completed with no issues +} + +/** + * Implementation of hook_form_alter(). + */ +function i18ncontent_form_alter(&$form, $form_state, $form_id) { + switch ($form_id) { + case 'node_type_form': + // Replace the help text default value in the node type form with data from + // i18nstrings. Help text is handled in hook_node_type() and hook_help(). + $type = $form['#node_type']->type; + if ($type) { + // Fetch default language node type help + $source = i18ncontent_node_help_source($type); + // We dont need to pass the value through i18nstrings_ts() + if (isset($source->source)) { + $form['submission']['help']['#default_value'] = $source->source; + } + } + break; + + case 'search_form': + // Advanced search form. Add language and localize content type names + if ($form['module']['#value'] == 'node' && !empty($form['advanced'])){ + // @todo Handle language search conditions + //$form['advanced']['language'] = _i18n_language_select(); + if (!empty($form['advanced']['type'])) { + foreach ($form['advanced']['type']['#options'] as $type => $name) { + $form['advanced']['type']['#options'][$type] = i18nstrings("nodetype:type:$type:name", $name); + } + } + } + break; + + default: + // Translate field names for title and body for the node edit form. + if (isset($form['#id']) && $form['#id'] == 'node-form') { + $type = $form['#node']->type; + if (!empty($form['title']['#title'])) { + $form['title']['#title'] = i18nstrings("nodetype:type:$type:title", $form['title']['#title']); + } + if (!empty($form['body_field']['body']['#title'])) { + $form['body_field']['body']['#title'] = i18nstrings("nodetype:type:$type:body", $form['body_field']['body']['#title']); + } + } + break; + } +} + +/** + * Implementation of hook_node_type(). + */ +function i18ncontent_node_type($op, $info) { + $language = language_default('language'); + if ($op == 'insert' || $op == 'update') { + if(!empty($info->old_type) && $info->old_type != $info->type) { + i18nstrings_update_context("nodetype:type:$old_type:*", "nodetype:type:$type:*"); + } + i18nstrings_update("nodetype:type:$info->type:name", $info->name); + i18nstrings_update("nodetype:type:$info->type:title", $info->title_label); + i18nstrings_update("nodetype:type:$info->type:body", $info->body_label); + i18nstrings_update("nodetype:type:$info->type:description", $info->description); + if (empty($info->help)) { + i18nstrings_remove("nodetype:type:$info->type:help"); + } else { + i18nstrings_ts("nodetype:type:$info->type:help", $info->help, $language, TRUE); + // Remove the 'help' text from {node_type} to avoid both the + // original text and translation appearing in hook_help(). + db_query("UPDATE {node_type} set help = '' WHERE type = '%s'", $info->type); + } + } + + if ($op == 'delete') { + i18nstrings_remove("nodetype:type:$info->type:title"); + i18nstrings_remove("nodetype:type:$info->type:name"); + i18nstrings_remove("nodetype:type:$info->type:description"); + i18nstrings_remove("nodetype:type:$info->type:body"); + i18nstrings_remove("nodetype:type:$info->type:help"); + } +} + + +/** + * Implementation of hook_menu_alter(). + * + * Take over the node add pages. + */ +function i18ncontent_menu_alter(&$menu) { + $menu['node/add']['page callback'] = 'i18ncontent_node_add_page'; + if (variable_get('page_manager_node_edit_disabled', TRUE)) { + // Replace node/add/* menu items + foreach (node_get_types('types', NULL, TRUE) as $type) { + $type_url_str = str_replace('_', '-', $type->type); + $path = 'node/add/' . $type_url_str; + if (!empty($menu[$path]['page callback']) && $menu[$path]['page callback'] == 'node_add') { + $menu[$path]['page callback'] = 'i18ncontent_node_add'; + $menu[$path]['title callback'] = 'i18nstrings_title_callback'; + $menu[$path]['title arguments'] = array('nodetype:type:'. $type->type .':name', $type->name); + } + } + } +} + +/** + * Replacement for node_add_page. + */ +function i18ncontent_node_add_page() { + $item = menu_get_item(); + $content = system_admin_menu_block($item); + // The node type isn't available in the menu path, but 'title' is equivalent + // to the human readable name, so check this against node_get_types() to get + // the correct values. First we build an array of node types indexed by names + $type_names = array_flip(node_get_types('names')); + foreach ($content as $key => $item) { + if ($type = $type_names[$item['link_title']]){ + // We just need to translate the description, the title is translated by the menu system + $content[$key]['description'] = i18nstrings("nodetype:type:$type:description", $item['description']); + } + } + return theme('node_add_list', $content); +} + +/** + * Replacement for node_add + * + * This just calls node_add() and switches title. This has to be done here to work always + */ +function i18ncontent_node_add($type) { + global $user; + + $types = node_get_types(); + $type = isset($type) ? str_replace('-', '_', $type) : NULL; + // If a node type has been specified, validate its existence. + if (isset($types[$type]) && node_access('create', $type)) { + // Initialize settings: + $node = array('uid' => $user->uid, 'name' => (isset($user->name) ? $user->name : ''), 'type' => $type, 'language' => ''); + + drupal_set_title(t('Create @name', array('@name' => i18nstrings("nodetype:type:$type:name", $types[$type]->name)))); + $output = drupal_get_form($type .'_node_form', $node); + } + + return $output; +} + +/** + * Fetch default source for node type help + */ +function i18ncontent_node_help_source($type) { + $context = i18nstrings_context("nodetype:type:$type:help"); + $source = i18nstrings_get_source($context); + return $source; +} diff --git a/sites/all/modules/i18n/i18nmenu/i18nmenu.info b/sites/all/modules/i18n/i18nmenu/i18nmenu.info new file mode 100644 index 0000000..0e26e48 --- /dev/null +++ b/sites/all/modules/i18n/i18nmenu/i18nmenu.info @@ -0,0 +1,15 @@ +name = Menu translation +description = Supports translatable custom menu items. +dependencies[] = i18n +dependencies[] = menu +dependencies[] = i18nblocks +dependencies[] = i18nstrings +package = Multilanguage +core = 6.x + +; Information added by drupal.org packaging script on 2011-10-11 +version = "6.x-1.10" +core = "6.x" +project = "i18n" +datestamp = "1318336004" + diff --git a/sites/all/modules/i18n/i18nmenu/i18nmenu.install b/sites/all/modules/i18n/i18nmenu/i18nmenu.install new file mode 100644 index 0000000..6696ad2 --- /dev/null +++ b/sites/all/modules/i18n/i18nmenu/i18nmenu.install @@ -0,0 +1,16 @@ + t('Menu')); + case 'info': + $info['menu']['refresh callback'] = 'i18nmenu_locale_refresh'; + $info['menu']['format'] = FALSE; + return $info; + } +} + +/** + * Refresh locale strings. + */ +function i18nmenu_locale_refresh() { + // Rebuild menus to ensure all items are altered in i18nmenu_menu_link_alter(). + menu_rebuild(); + i18n_selection_mode('off'); + foreach (menu_get_menus() as $name => $title) { + $tree = menu_tree_all_data($name); + i18nmenu_localize_tree($tree, TRUE); + } + i18n_selection_mode('reset'); + return TRUE; // Meaning it completed with no issues +} + +/** + * Implementation of hook_menu_link_alter(). + * + * Catch changed links, update language and set alter option. + */ +function i18nmenu_menu_link_alter(&$item, $menu) { + // If we set option to language it causes an error with the link system + // This should handle language only as the links are being manually updated + if (!empty($item['language'])) { + $item['options']['langcode'] = $item['language']; + } + elseif (isset($item['language'])) { + unset($item['options']['langcode']); + } + // If we are handling custom menu items of menu module and no language is set, + // invoke translation via i18nstrings module. + if (empty($item['language']) && $item['module'] == 'menu') { + // Set title_callback to FALSE to avoid calling t(). + $item['title_callback'] = FALSE; + // Setting the alter option to true ensures that + // hook_translated_menu_link_alter() will be called. + $item['options']['alter'] = TRUE; + } +} + +/** + * Implementation of hook_translated_menu_link_alter(). + * + * Translate menu links on the fly. + * + * @see i18nmenu_menu_link_alter() + */ +function i18nmenu_translated_menu_link_alter(&$item, $map) { + if ($item['module'] == 'menu') { + $item['title'] = _i18nmenu_get_item_title($item); + $item['localized_options']['attributes']['title'] = _i18nmenu_get_item_description($item); + } +} + +/** + * Implementation of hook_help(). + */ +function i18nmenu_help($path, $arg) { + switch ($path) { + case 'admin/help#i18nmenu' : + $output = ''. t('This module provides support for translatable custom menu items:') .'
'; + $output .= ''. t('To search and translate strings, use the translation interface pages.', array('@translate-interface' => url('admin/build/translate'))) .'
'; + return $output; + } +} + +/** + * Get localized menu tree. + */ +function i18nmenu_translated_tree($menu_name) { + static $menu_output = array(); + + if (!isset($menu_output[$menu_name])) { + $tree = menu_tree_page_data($menu_name); + i18nmenu_localize_tree($tree); + $menu_output[$menu_name] = menu_tree_output($tree); + } + return $menu_output[$menu_name]; +} + +/** + * Localize menu tree. + */ +function i18nmenu_localize_tree(&$tree, $update = FALSE) { + global $language; + foreach ($tree as $index => $item) { + $link = $item['link']; + if ($link['customized']) { + // Remove links for other languages than current. + // Links with language wont be localized. + if (!empty($link['options']['langcode'])) { + if ($link['options']['langcode'] != $language->language) { + unset($tree[$index]); + } + } + else { + $router = i18nmenu_get_router($link['router_path']); + // If the title is the same it will be localized by the menu system. + if ($link['link_title'] != $router['title']) { + $tree[$index]['link']['title'] = _i18nmenu_get_item_title($link, $update); + } + $tree[$index]['link']['localized_options']['attributes']['title'] = _i18nmenu_get_item_description($link, $update); + // Localize subtree. + if ($item['below'] !== FALSE) { + i18nmenu_localize_tree($tree[$index]['below'], $update); + } + } + } + } +} + +/** + * Return an array of localized links for a navigation menu. + */ +function i18nmenu_menu_navigation_links($menu_name, $level = 0) { + // Don't even bother querying the menu table if no menu is specified. + if (empty($menu_name)) { + return array(); + } + + // Get the menu hierarchy for the current page. + $tree = menu_tree_page_data($menu_name); + i18nmenu_localize_tree($tree); + + // Go down the active trail until the right level is reached. + while ($level-- > 0 && $tree) { + // Loop through the current level's items until we find one that is in trail. + while ($item = array_shift($tree)) { + if ($item['link']['in_active_trail']) { + // If the item is in the active trail, we continue in the subtree. + $tree = empty($item['below']) ? array() : $item['below']; + break; + } + } + } + + // Create a single level of links. + $links = array(); + foreach ($tree as $item) { + if (!$item['link']['hidden']) { + $class = ''; + $l = $item['link']['localized_options']; + $l['href'] = $item['link']['href']; + $l['title'] = $item['link']['title']; + if ($item['link']['in_active_trail']) { + $class = ' active-trail'; + } + // Keyed with the unique mlid to generate classes in theme_links(). + $links['menu-'. $item['link']['mlid'] . $class] = $l; + } + } + return $links; +} + +/** + * Replace standard primary and secondary links. + */ +function i18nmenu_preprocess_page(&$vars) { + if (theme_get_setting('toggle_primary_links')) { + $vars['primary_links'] = i18nmenu_menu_navigation_links(variable_get('menu_primary_links_source', 'primary-links')); + } + + // If the secondary menu source is set as the primary menu, we display the + // second level of the primary menu. + + if (theme_get_setting('toggle_secondary_links')) { + if (variable_get('menu_secondary_links_source', 'secondary-links') == variable_get('menu_primary_links_source', 'primary-links')) { + $vars['secondary_links'] = i18nmenu_menu_navigation_links(variable_get('menu_primary_links_source', 'primary-links'), 1); + } + else { + $vars['secondary_links'] = i18nmenu_menu_navigation_links(variable_get('menu_secondary_links_source', 'secondary-links'), 0); + } + } +} + +/** + * Optionally insert/update and return a localized menu item title. + */ +function _i18nmenu_get_item_title($link, $update = FALSE, $langcode = NULL) { + $key = 'menu:item:'. $link['mlid'] .':title'; + if ($update) { + i18nstrings_update($key, $link['link_title']); + } + return i18nstrings($key, $link['link_title'], $langcode); +} + +/** + * Optionally insert/update and return a localized menu item description. + */ +function _i18nmenu_get_item_description($link, $update = FALSE, $langcode = NULL) { + if (empty($link['options']['attributes']['title'])) { + return; + } + $key = 'menu:item:'. $link['mlid'] .':description'; + $description = $link['options']['attributes']['title']; + if ($update) { + i18nstrings_update($key, $description); + } + return i18nstrings($key, $description, $langcode); +} + +/** + * Delete a menu item translation. + */ +function _i18nmenu_delete_item($mlid) { + i18nstrings_remove_string('menu:item:'. $mlid .':title'); + i18nstrings_remove_string('menu:item:'. $mlid .':description'); +} + +/** + * Get the menu router for this router path. + * + * We need the untranslated title to compare, and this will be fast. + * There's no api function to do this? + */ +function i18nmenu_get_router($path) { + static $cache = array(); + if (!array_key_exists($path, $cache)) { + $cache[$path] = db_fetch_array(db_query("SELECT title FROM {menu_router} WHERE path = '%s'", $path)); + } + return $cache[$path]; +} + +/** + * Implementation of hook_form_form_id_alter(). + * + * Register a submit callback to process menu title. + */ +function i18nmenu_form_menu_edit_menu_alter(&$form, $form_state) { + $form['#submit'][] = 'i18nmenu_menu_edit_menu_submit'; +} + +/** + * Submit handler for the menu_edit_item form. + * + * On menu item insert or update, save a translation record. + */ +function i18nmenu_menu_edit_menu_submit($form, &$form_state) { + // Ensure we have a menu to work with. + if (isset($form_state['values']['menu_name']) && isset($form_state['values']['title'])) { + if ($form['#insert']) { + $context = 'menu:menu:menu-' . $form_state['values']['menu_name'] . ':title'; + } else { + $context = 'menu:menu:' . $form_state['values']['menu_name'] . ':title'; + } + i18nstrings_update_string($context, $form_state['values']['title']); + } +} + +/** + * Implementation of hook_form_form_id_alter(). + * + * Add a language selector to the menu_edit_item form and register a submit + * callback to process items. + */ +function i18nmenu_form_menu_edit_item_alter(&$form, $form_state) { + if ($form['menu']['#item'] && isset($form['menu']['#item']['options']['langcode'])) { + $language = $form['menu']['#item']['options']['langcode']; + } + else { + $language = ''; + } + $form['menu']['language'] = array( + '#type' => 'select', + '#title' => t('Language'), + '#description' => t('Select a language for this menu item. Choose "All languages" to make the menu item translatable into different languages.'), + '#options' => array('' => t('All languages')) + locale_language_list('name'), + '#default_value' => $language, + ); + array_unshift($form['#validate'], 'i18nmenu_menu_item_prepare_normal_path'); + $form['#submit'][] = 'i18nmenu_menu_item_update'; +} + +/** + * 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. + */ +function i18nmenu_menu_item_prepare_normal_path($form, &$form_state) { + $item = &$form_state['values']['menu']; + $normal_path = drupal_get_normal_path($item['link_path'], $item['language']); + if ($item['link_path'] != $normal_path) { + drupal_set_message(t('The menu system stores system paths only, but will use the URL alias for display. %link_path has been stored as %normal_path', array('%link_path' => $item['link_path'], '%normal_path' => $normal_path))); + $item['link_path'] = $normal_path; + } +} + +/** + * Submit handler for the menu_edit_item form. + * + * On menu item insert or update, save a translation record. + */ +function i18nmenu_menu_item_update($form, &$form_state) { + // Ensure we have a menu item to work with. + if (isset($form_state['values']['menu'])) { + $item = $form_state['values']['menu']; + _i18nmenu_update_item($item); + } +} + +/** + * Update the translation data for a menu item that has been inserted + * or updated. + * + * @see i18nmenu_menu_item_update() + * @see i18nmenu_nodeapi() + */ +function _i18nmenu_update_item($item) { + list($item['menu_name'], $item['plid']) = explode(':', $item['parent']); + // If this was an insert, determine the ID that was set. + if (!isset($item['mlid'])) { + $item['mlid'] = db_result(db_query("SELECT MAX(mlid) FROM {menu_links} WHERE link_path = '%s' AND menu_name = '%s' AND module = 'menu' AND plid = %d AND link_title = '%s'", $item['link_path'], $item['menu_name'], $item['plid'], $item['link_title'])); + } + if (!empty($item['mlid'])) { + _i18nmenu_get_item_title($item, TRUE); + _i18nmenu_get_item_description($item, TRUE); + } +} + +/** + * Helper function: load the menu item associated to the current node. + */ +function _i18nmenu_node_prepare(&$node) { + menu_nodeapi($node, 'prepare'); +} + +/** + * Implementation of hook_form_form_id_alter(). + * + * Add a submit handler to the the menu item deletion confirmation form. + */ +function i18nmenu_form_menu_item_delete_form_alter(&$form, $form_state) { + $form['#submit'][] = 'i18nmenu_item_delete_submit'; +} + +/** + * Submit function for the delete button on the menu item editing form. + */ +function i18nmenu_item_delete_submit($form, &$form_state) { + _i18nmenu_delete_item($form['#item']['mlid']); +} + +/** + * Implementation of hook_form_alter(). + * + * Add language to menu settings of the node form, as well as setting defaults + * to match the translated item's menu settings. + */ +function i18nmenu_form_alter(&$form, $form_state, $form_id) { + if (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] .'_node_form' == $form_id) { + $node = $form['#node']; + + if (!empty($form['menu'])) { + // Customized must be set to 1 to save language. + $form['menu']['customized'] = array('#type' => 'value', '#value' => 1); + } + + // Do nothing if the node already has a menu. + if (!empty($node->menu['mlid'])) { + return; + } + + // Find the translation source node. If creating a new node, + // translation_source is set. Otherwise, node_load the tnid. + // New translation. + if (!empty($node->translation_source)) { + $tnode = $node->translation_source; + } + // Editing existing translation. + elseif (!empty($node->nid) && !empty($node->tnid) && $node->nid != $node->tnid) { + $tnode = node_load($node->tnid); + } + // If no translation node, return. + else { + return; + } + + // Prepare the tnode so the menu item will be available. + _i18nmenu_node_prepare($tnode); + + if ($tnode->menu) { + // Set default values based on translation source's menu. + $form['menu']['link_title']['#default_value'] = $tnode->menu['link_title']; + $form['menu']['weight']['#default_value'] = $tnode->menu['weight']; + $form['menu']['parent']['#default_value'] = $tnode->menu['menu_name'] .':'. $tnode->menu['plid']; + } + } +} + +/** + * Implementation of hook_nodeapi(). + * + * Save or delete menu item strings associated with nodes. + */ +function i18nmenu_nodeapi(&$node, $op) { + switch ($op) { + case 'presave': + // Ensure that the menu item language always matches node language. + if (isset($node->menu) && isset($node->language)) { + $node->menu['language'] = $node->language; + } + break; + case 'insert': + case 'update': + if (isset($node->menu)) { + $item = $node->menu; + if (!empty($item['delete'])) { + _i18nmenu_delete_item($item['mlid']); + } + elseif (trim($item['link_title'])) { + $item['link_title'] = trim($item['link_title']); + $item['link_path'] = "node/$node->nid"; + _i18nmenu_update_item($item); + } + } + break; + case 'delete': + // Delete all menu item link translations that point to this node. + $result = db_query("SELECT mlid FROM {menu_links} WHERE link_path = 'node/%d' AND module = 'menu'", $node->nid); + while ($m = db_fetch_array($result)) { + _i18nmenu_delete_item($m['mlid']); + } + break; + case 'prepare translation': + if (empty($node->menu['mlid']) && !empty($node->translation_source)) { + $tnode = $node->translation_source; + // Prepare the tnode so the menu item will be available. + node_object_prepare($tnode); + $node->menu['link_title'] = $tnode->menu['link_title']; + $node->menu['weight'] = $tnode->menu['weight']; + } + break; + } +} diff --git a/sites/all/modules/i18n/i18npoll/i18npoll.info b/sites/all/modules/i18n/i18npoll/i18npoll.info new file mode 100644 index 0000000..51284bd --- /dev/null +++ b/sites/all/modules/i18n/i18npoll/i18npoll.info @@ -0,0 +1,15 @@ +name = Poll aggregate +description = Aggregates poll results for all translations. +dependencies[] = translation +dependencies[] = poll +package = Multilanguage +core = 6.x + + + +; Information added by drupal.org packaging script on 2011-10-11 +version = "6.x-1.10" +core = "6.x" +project = "i18n" +datestamp = "1318336004" + diff --git a/sites/all/modules/i18n/i18npoll/i18npoll.module b/sites/all/modules/i18n/i18npoll/i18npoll.module new file mode 100644 index 0000000..55fec86 --- /dev/null +++ b/sites/all/modules/i18n/i18npoll/i18npoll.module @@ -0,0 +1,169 @@ +tnid) && ($vote = i18npoll_get_vote($node->tnid))) { + $form['#i18ntnid'] = $node->tnid; + $form['#nid'] = $vote->nid; + } + } + } +} + +/** + * Implementation of hook_nodeapi(). + * + * Replaces poll results with aggregated translations. + * + * We don't add all language results on loading to avoid the data being trashed + * when editing and saving nodes again. + */ +function i18npoll_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) { + global $user; + + if ($node->type == 'poll' && !empty($node->tnid)) { + // Replace results for node view + if ($op == 'view' && (empty($node->allowvotes) || !empty($node->show_results))) { + $node->content['body'] = array( + '#value' => i18npoll_view_results($node, $teaser, $page, FALSE), + ); + } + // Check again whether or not this user is allowed to vote. + // User may have voted for any of the translations + if ($op == 'load' && $node->allowvotes) { + $result = i18npoll_get_vote($node->tnid); + if (isset($result->chorder)) { + $node->vote = $result->chorder; + $node->allowvotes = FALSE; + } + } + } +} + +/** + * Implementation of hook_block(). + * + * Generates a block containing the latest poll with aggregated results. + */ +function i18npoll_block($op = 'list', $delta = 0) { + if (user_access('access content')) { + if ($op == 'list') { + $blocks[0]['info'] = t('Most recent poll (Aggregated translations)'); + return $blocks; + } + elseif ($op == 'view') { + // Retrieve the latest poll. + $sql = db_rewrite_sql("SELECT MAX(n.created) FROM {node} n INNER JOIN {poll} p ON p.nid = n.nid WHERE n.status = 1 AND p.active = 1"); + $timestamp = db_result(db_query($sql)); + if ($timestamp) { + $poll = node_load(array('type' => 'poll', 'created' => $timestamp, 'status' => 1)); + + if ($poll->nid) { + $poll = i18npoll_view($poll, TRUE, FALSE, TRUE); + } + } + $block['subject'] = t('Poll'); + $block['content'] = drupal_render($poll->content); + return $block; + } + } +} + +/** + * Implementation of hook_view(). + * + * @param $block + * An extra parameter that adapts the hook to display a block-ready + * rendering of the poll. + */ +function i18npoll_view($node, $teaser = FALSE, $page = FALSE, $block = FALSE) { + global $user; + $output = ''; + + // Special display for side-block. + if ($block) { + // No 'read more' link. + $node->readmore = FALSE; + + $links = module_invoke_all('link', 'node', $node, 1); + $links[] = array( + 'title' => t('Older polls'), + 'href' => 'poll', + 'attributes' => array( + 'title' => t('View the list of polls on this site.') + ) + ); + if ($node->allowvotes && $block) { + $links[] = array( + 'title' => t('Results'), + 'href' => 'node/'. $node->nid .'/results', + 'attributes' => array( + 'title' => t('View the current poll results.') + ) + ); + } + + $node->links = $links; + } + + if (!empty($node->allowvotes) && ($block || empty($node->show_results))) { + $node->content['body'] = array( + '#value' => drupal_get_form('poll_view_voting', $node, $block), + ); + } + else { + $node->content['body'] = array( + '#value' => i18npoll_view_results($node, $teaser, $page, $block), + ); + } + return $node; +} + +/** + * Generates a graphical representation of the results of a poll. + */ +function i18npoll_view_results(&$node, $teaser, $page, $block) { + // Load the appropriate choices into the $poll object. + $result = db_query("SELECT c.chorder, SUM(c.chvotes) AS votes FROM {poll_choices} c INNER JOIN {node} n ON c.nid = n.nid WHERE n.tnid = %d GROUP BY c.chorder", $node->tnid); + while ($choice = db_fetch_object($result)) { + // If this option not set for the source node, do not show. + if (isset($node->choice[$choice->chorder])) { + $node->choice[$choice->chorder]['chvotes'] = $choice->votes; + } + } + return poll_view_results($node, $teaser, $page, $block); +} + +/** + * Get user vote for this node or its translations. + * + * Returns object with nid, chorder. Has static caching as this will typically be called twice. + */ +function i18npoll_get_vote($tnid) { + global $user; + static $vote = array(); + if (!array_key_exists($tnid, $vote)) { + if ($user->uid) { + $vote[$tnid] = db_fetch_object(db_query('SELECT v.nid, v.chorder FROM {poll_votes} v INNER JOIN {node} n ON n.nid = v.nid WHERE n.tnid = %d AND v.uid = %d', $tnid, $user->uid)); + } + else { + $vote[$tnid] = db_fetch_object(db_query("SELECT v.chorder FROM {poll_votes} v INNER JOIN {node} n ON n.nid = v.nid WHERE n.tnid = %d AND v.hostname = '%s'", $tnid, ip_address())); + } + } + return $vote[$tnid]; +} \ No newline at end of file diff --git a/sites/all/modules/i18n/i18nprofile/i18nprofile.info b/sites/all/modules/i18n/i18nprofile/i18nprofile.info new file mode 100644 index 0000000..95eb27a --- /dev/null +++ b/sites/all/modules/i18n/i18nprofile/i18nprofile.info @@ -0,0 +1,13 @@ +name = Profile translation +description = Enables multilingual profile fields. +dependencies[] = profile +dependencies[] = i18nstrings +package = Multilanguage +core = 6.x + +; Information added by drupal.org packaging script on 2011-10-11 +version = "6.x-1.10" +core = "6.x" +project = "i18n" +datestamp = "1318336004" + diff --git a/sites/all/modules/i18n/i18nprofile/i18nprofile.install b/sites/all/modules/i18n/i18nprofile/i18nprofile.install new file mode 100644 index 0000000..e0cc962 --- /dev/null +++ b/sites/all/modules/i18n/i18nprofile/i18nprofile.install @@ -0,0 +1,68 @@ +name", $field, array('title', 'explanation', 'options')); + if (!in_array($field->category, $categories)) { + $categories[] = $field->category; + i18nstrings_update("profile:category", $field->category); + } + } + // Category translations from variables. + foreach (array_keys(language_list()) as $lang) { + if ($translation = variable_get('i18nprofile_'. $lang, FALSE)) { + foreach ($translation as $category => $translation) { + if (in_array($category, $categories) && $translation) { + $context = i18nstrings_context('profile:category', $category); + i18nstrings_update_translation($context, $lang, $translation); + } + } + } + } + // Move current data into string translations. + $result = db_query("SELECT * FROM {i18n_profile_fields}"); + while ($field = db_fetch_object($result)) { + foreach (array('title', 'explanation', 'options') as $property) { + if (!empty($field->$property)) { + i18nstrings_update_translation("profile:field:$field->name:$property", $field->language, $field->$property); + } + } + } + + return $items; +} + +// Clean up. Uncomment when it all works. +/* +function i18nprofile_update_3() { + $items[] = update_sql("DROP TABLE {i18n_profile_fields};"); + + foreach (array_keys(language_list()) as $lang) { + variable_del('i18nprofile_'.$lang); + } + return $items; +}*/ \ No newline at end of file diff --git a/sites/all/modules/i18n/i18nprofile/i18nprofile.module b/sites/all/modules/i18n/i18nprofile/i18nprofile.module new file mode 100644 index 0000000..4be3bbb --- /dev/null +++ b/sites/all/modules/i18n/i18nprofile/i18nprofile.module @@ -0,0 +1,255 @@ +'. t('Supports translation for profile module field names and descriptions.') .''; + $output .= ''. t('To search and translate strings, use the translation interface pages.', array('@translate-interface' => url('admin/build/translate'))) .'
'; + return $output; + } +} + +/** + * Implementation of hook_locale(). + */ +function i18nprofile_locale($op = 'groups', $group = NULL) { + switch ($op) { + case 'groups': + return array('profile' => t('Profile')); + case 'info': + $info['profile']['refresh callback'] = 'i18nprofile_locale_refresh'; + $info['profile']['format'] = FALSE; + return $info; + } +} + +/** + * Refresh strings. + */ +function i18nprofile_locale_refresh() { + $result = db_query('SELECT * FROM {profile_fields}'); + $categories = array(); + while ($field = db_fetch_object($result)) { + // Store strings to translate: title, explanation, options. + i18nstrings_update_object("profile:field:$field->name", $field, array('title', 'explanation', 'options')); + // Store category if not there yet. + if (!isset($categories[$field->category])) { + i18nstrings_update("profile:category", $field->category); + $categories[$field->category] = 1; + } + } + return TRUE; // Meaning it completed with no issues +} + +/** + * Implementation of hook_menu_alter(). + * + * Replace title callbacks for profile categories. + */ +function i18nprofile_menu_alter(&$items) { + $empty_account = new stdClass(); + if (($categories = _user_categories($empty_account)) && (count($categories) > 1)) { + foreach ($categories as $key => $category) { + // 'account' is already handled by the MENU_DEFAULT_LOCAL_TASK. + $path = 'user/%user_category/edit/'. $category['name']; + if ($category['name'] != 'account' && !empty($items[$path])) { + $items[$path]['title callback'] = 'i18nprofile_translate_category'; // Was 'check_plain', + $items[$path]['title arguments'] = array($category['title']); // Was array($category['title']) + } + } + } +} + +function i18nprofile_translate_category($title) { + return check_plain(i18nstrings('profile:category', $title)); +} + +/** + * Implementation of hook_profile_alter(). + * + * Translates categories and fields. + */ +function i18nprofile_profile_alter(&$account) { + foreach (profile_categories() as $category) { + $name = $category['name']; + if (!empty($account->content[$name])) { + // First ranslate category title then fields. + $account->content[$name]['#title'] = i18nstrings('profile:category', $account->content[$name]['#title']); + foreach (element_children($account->content[$name]) as $field) { + i18nprofile_form_translate_field($account->content[$name], $field); + // Translate value if options field + if (!empty($account->content[$name][$field]['#value']) && $options = i18nprofile_field_options($field)) { + // Get the value from the account because this one may have been formatted. + if (isset($options[$account->$field])) { + // It may be a link or a paragraph, trick for not loading the field again. + if (!preg_match('|^name:$property"); + } + // Delete category too if no more fields in the same category + if (!db_result(db_query("SELECT COUNT(*) FROM {profile_fields} WHERE category = '%s'", $field->category))) { + i18nstrings_remove_string("profile:category", $values->category); + } +} + +/** + * Process profile_field_form submissions. + */ +function i18nprofile_field_form_submit($form, &$form_state) { + $values = (object)$form_state['values']; + // Check old field name in case it has changed. + $oldname = $form['fields']['name']['#default_value']; + if ($oldname != 'profile_' && $oldname != $values->name) { + i18nstrings_update_context("profile:field:$oldname:*", "profile:field:$values->name:*"); + } + // Store category. + i18nstrings_update("profile:category", $values->category); + // Store strings to translate: title, explanation, options. + i18nstrings_update_object("profile:field:$values->name", $values, array('title', 'explanation', 'options')); +} + +/** + * Translate form fields for a given category. + */ +function i18nprofile_form_translate_category(&$form, $category) { + if (!empty($form[$category])) { + $form[$category]['#title'] = i18nstrings('profile:category', $form[$category]['#title']); + foreach (element_children($form[$category]) as $field) { + i18nprofile_form_translate_field($form[$category], $field); + } + } +} + +/** + * Translate form field. + */ +function i18nprofile_form_translate_field(&$form, $field) { + if (!empty($form[$field]['#title'])) { + $form[$field]['#title'] = i18nstrings("profile:field:$field:title", $form[$field]['#title']); + } + elseif (!empty($form[$field]['#value'])) { + // Special treating for checboxes. + $field_type = db_result(db_query("SELECT type FROM {profile_fields} WHERE name = '%s'", $field)); + if ($field_type == 'checkbox') { + $form[$field]['#value'] = i18nstrings("profile:field:$field:title", $form[$field]['#value']); + } + } + + if (!empty($form[$field]['#description'])) { + $form[$field]['#description'] = i18nstrings("profile:field:$field:explanation", $form[$field]['#description']); + } + if (!empty($form[$field]['#options'])) { + if ($options = i18nprofile_field_options($field, $form[$field]['#options'])) { + $form[$field]['#options'] = $options; + } + } + +} + +/** + * Translates field options. + */ +function i18nprofile_field_options($field, $source = array()) { + if ($translation = i18nstrings("profile:field:$field:options", '')) { + // Troubles when doing the split, produces empty lines, quick fix + $translation = str_replace("\r", '', $translation); + $translation = explode("\n", $translation); + if ($source) { + $options = $source; + } + elseif ($source = db_result(db_query("SELECT options FROM {profile_fields} WHERE name = '%s'", $field))) { + $source = str_replace("\r", '', $source); + $source = explode("\n", $source); + $options = array(); + } + else { + return NULL; + } + foreach ($source as $value) { + if ($value != '--') { + $string = $translation ? trim(array_shift($translation)) : trim($value); + $options[trim($value)] = $string; + } + } + return $options; + } +} + +/** + * Translate form fields for all categories. + * + * This is useful when we don't know which categories we have, like in the user register form. + */ +function i18nprofile_form_translate_all($form_id, &$form) { + $categories = profile_categories(); + if (is_array($categories)) { + foreach ($categories as $category) { + if (isset($form[$category['name']])) { + i18nprofile_form_translate_category( $form, $category['name']); + } + } + } +} diff --git a/sites/all/modules/i18n/i18nstrings/i18nstrings.admin.inc b/sites/all/modules/i18n/i18nstrings/i18nstrings.admin.inc new file mode 100644 index 0000000..9e46e1e --- /dev/null +++ b/sites/all/modules/i18n/i18nstrings/i18nstrings.admin.inc @@ -0,0 +1,148 @@ + 'checkboxes', + '#title' => t('Select text groups'), + '#options' => $groups, + '#description' => t('If a text group is no showing up here it means this feature is not implemented for it.'), + ); + $form['refresh'] = array( + '#type' => 'submit', + '#value' => t('Refresh strings'), + '#suffix' => ''. t('This will create all the missing strings for the selected text groups.') .'
', + ); + // Get all languages, except default language. + $languages = locale_language_list('name', TRUE); + unset($languages[language_default('language')]); + $form['languages'] = array( + '#type' => 'checkboxes', + '#title' => t('Select languages'), + '#options' => $languages, + ); + $form['update'] = array( + '#type' => 'submit', + '#value' => t('Update translations'), + '#suffix' => ''. t('This will fetch all existing translations from the localization tables for the selected text groups and languages.') .'
', + ); + return $form; +} + +/** + * Form submission. + */ +function i18nstrings_admin_refresh_submit($form, &$form_state) { + $op = isset($form_state['values']['op']) ? $form_state['values']['op'] : ''; + $groups = array_filter($form_state['values']['groups']); + $languages = array_filter($form_state['values']['languages']); + $group_names = module_invoke_all('locale', 'groups'); + if ($op == t('Refresh strings') && $groups) { + foreach ($groups as $group) { + if (i18nstrings_refresh_group($group, TRUE)) { + drupal_set_message(t("Successfully refreshed strings for %group", array('%group' => $group_names[$group]))); + } + else { + drupal_set_message(t("Cannot refresh strings for %group.", array('%group' => $group_names[$group])), 'warning'); + } + } + } + elseif ($op == t('Update translations') && $groups && $languages) { + $count = 0; + foreach ($languages as $language) { + $count += i18nstrings_admin_update($language, $groups); + } + drupal_set_message(format_plural($count, '1 string has been updated.', '@count strings have been updated.')); + } +} + +/** + * Update strings for language. + */ +function i18nstrings_admin_update($language, $groups) { + $params = $groups; + $params[] = $language; + $sql = 'SELECT g.*, t.translation, t.lid as tlid, i.format FROM {locales_source} g INNER JOIN {locales_source} s ON g.source = s.source AND s.lid <> g.lid '; + $sql .= 'INNER JOIN {locales_target} t ON s.lid = t.lid LEFT JOIN {locales_target} t2 ON g.lid = t2.lid '; + $sql .= 'INNER JOIN {i18n_strings} i ON i.lid = g.lid '; + $sql .= 'WHERE t2.lid IS NULL AND g.textgroup IN ('. db_placeholders($groups, 'varchar') .") AND t.language = '%s'"; + $result = db_query($sql, $params); + $count = 0; + while ($string = db_fetch_object($result)) { + // Just update strings when no input format, otherwise it could be dangerous under some circumstances. + if (empty($string->format) && !empty($string->translation)) { + $count++; + db_query("INSERT INTO {locales_target} (translation, lid, language) VALUES('%s', %d, '%s')", $string->translation, $string->lid, $language); + } + } + return $count; +} + +/** + * Configure filters for string translation. + * + * This has serious security implications so this form needs the 'administer filters' permission + */ +function i18nstrings_admin_settings() { + include_once './includes/locale.inc'; + // As the user has administer filters permissions we get a full list here + foreach (filter_formats() as $fid => $format) { + $format_list[$fid] = $format->name; + } + $form['i18nstrings_allowed_formats'] = array( + '#title' => t('Translatable input formats'), + '#options' => $format_list, + '#type' => 'checkboxes', + '#default_value' => variable_get('i18nstrings_allowed_formats', array(variable_get('filter_default_format', 1))), + '#description' => t('Only the strings that have the input formats selected will be allowed by the translation system. All the others will be deleted next time the strings are refreshed.'), + ); + // Whitelist text groups without formatted strings for backwards compatibility + $textgroups = module_invoke_all('locale', 'groups'); + unset($textgroups['default']); + foreach (array_keys($textgroups) as $group) { + $format = i18nstrings_group_info($group, 'format'); + if (isset($format)) { + // This one already has format information, so remove from list + unset($textgroups[$group]); + } + } + // If there are 'old' textgroups, display the bypass option + if ($textgroups) { + $form['i18nstrings_allowed_textgroups'] = array( + '#title' => t('Safe text groups'), + '#options' => $textgroups, + '#type' => 'checkboxes', + '#default_value' => variable_get('i18nstrings_allowed_textgroups', array()), + '#description' => t('Select text groups to bypass filter format checking. . It is unsafe to check this option unless you are sure all the strings from that text groups are safe for translators. This option is just for backwards compatibility until all the contributed modules implement the new strings API.'), + ); + } + elseif (variable_get('i18nstrings_allowed_textgroups', 0)) { + // Just in case there's a leftover variable before we updated some of the modules + variable_del('i18nstrings_allowed_textgroups'); + } + $form['array_filter'] = array('#type' => 'value', '#value' => TRUE); + return system_settings_form($form); +} diff --git a/sites/all/modules/i18n/i18nstrings/i18nstrings.info b/sites/all/modules/i18n/i18nstrings/i18nstrings.info new file mode 100644 index 0000000..6c0a083 --- /dev/null +++ b/sites/all/modules/i18n/i18nstrings/i18nstrings.info @@ -0,0 +1,12 @@ +name = String translation +description = Provides support for translation of user defined strings. +dependencies[] = locale +package = Multilanguage +core = 6.x + +; Information added by drupal.org packaging script on 2011-10-11 +version = "6.x-1.10" +core = "6.x" +project = "i18n" +datestamp = "1318336004" + diff --git a/sites/all/modules/i18n/i18nstrings/i18nstrings.install b/sites/all/modules/i18n/i18nstrings/i18nstrings.install new file mode 100644 index 0000000..e64956d --- /dev/null +++ b/sites/all/modules/i18n/i18nstrings/i18nstrings.install @@ -0,0 +1,219 @@ + 'int', 'not null' => TRUE, 'default' => 0)); + + // Add custom index to locales_source table. + db_add_index($ret, 'locales_source', 'textgroup_location', array(array('textgroup', 30), 'location')); +} + +/** + * Implementation of hook_uninstall(). + */ +function i18nstrings_uninstall() { + $ret = array(); + + // Create database tables. + drupal_uninstall_schema('i18nstrings'); + // @TODO locale table cleanup, think about it. + // Should we drop all strings for groups other than 'default' ? + + // Drop custom field. + db_drop_field($ret, 'locales_target', 'i18n_status'); + // Drop custom index. + db_drop_index($ret, 'locales_source', 'textgroup_location'); + // May be a left over variable + variable_del('i18nstrings_update_skip'); +} + +/** + * Implementation of hook_schema(). + */ +function i18nstrings_schema() { + $schema['i18n_strings'] = array( + 'description' => 'Metadata for source strings.', + 'fields' => array( + 'lid' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Source string ID. References {locales_source}.lid.', + ), + 'objectid' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'description' => 'Object ID.', + ), + 'type' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'description' => 'Object type for this string.', + ), + 'property' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'description' => 'Object property for this string.', + ), + 'objectindex' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Integer value of Object ID.', + ), + 'format' => array( + 'description' => "The input format used by this string.", + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0 + ), + + ), + 'primary key' => array('lid'), + ); + return $schema; +} + +/** + * Implementation of hook_schema_alter(). + */ +function i18nstrings_schema_alter(&$schema) { + // Add index for textgroup and location to {locales_source}. + $schema['locales_source']['indexes']['textgroup_location'] = array(array('textgroup', 30), 'location'); + // Add field for tracking whether translations need updating. + $schema['locales_target']['fields']['i18n_status'] = array( + 'description' => 'A boolean indicating whether this translation needs to be updated.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ); +} + +/** + * Update from 5.x version + */ +function i18nstrings_update_5900() { + i18nstrings_install(); + // Mark next update to be skipped + variable_set('i18nstrings_update_skip', 1); + return array(); +} + +/** + * Add new 'oid' column to i18n_strings table. + */ +/* +function i18nstrings_update_6000() { + $ret = array(); + db_add_field($ret, 'i18n_strings', 'oid', array('type' => 'int', 'not null' => TRUE, 'default' => 0)); + return $ret; +} +*/ + +/** + * Drupal 6 update. Change field name, 'oid' was psql reserved name. + * + * ALTER TABLE `drupal6_i18n`.`i18n_strings` CHANGE COLUMN `oid` `objectid` INTEGER NOT NULL DEFAULT 0; + */ +function i18nstrings_update_6001() { + $ret = array(); + if (!variable_get('i18nstrings_update_skip', 0)) { + db_change_field($ret, 'i18n_strings', 'oid', 'objectid', array('type' => 'int', 'not null' => TRUE, 'default' => 0)); + } + return $ret; +} + +/** + * Add index to {locales_source}. + */ +function i18nstrings_update_6002() { + $ret = array(); + if (!variable_get('i18nstrings_update_skip', 0)) { + db_add_index($ret, 'locales_source', 'textgroup_location', array(array('textgroup', 30), 'location')); + } + return $ret; +} + +/** + * Create i18n_strings_status schema. + * Add a field to track whether a translation needs updating. + */ +function i18nstrings_update_6003() { + $ret = array(); + if (!variable_get('i18nstrings_update_skip', 0)) { + db_add_field($ret, 'locales_target', 'status', array('type' => 'int', 'not null' => TRUE, 'default' => 0)); + } + return $ret; +} + +/** + * TODO: Add new D6 columns to i18n_strings table. + */ +/* +function i18nstrings_update_6004() { + $ret = array(); + + // Remove D5 primary keys (`strid`,`locale`). + db_drop_primary_key($ret, 'i18n_strings'); + + // Add new lid field and add primary key back. + db_add_field($ret, 'i18n_strings', 'lid', array('type' => 'int', 'not null' => TRUE, 'default' => 0)); + db_add_primary_key($ret, 'i18n_strings', array('lid')); + + db_add_field($ret, 'i18n_strings', 'type', array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => '')); + db_add_field($ret, 'i18n_strings', 'property', array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => 'default')); + + return $ret; +} +*/ + +/** + * Add objectindex, format fields, change objectid to varchar + */ +function i18nstrings_update_6005() { + $ret = array(); + if (!variable_get('i18nstrings_update_skip', 0)) { + db_add_field($ret, 'i18n_strings', 'objectindex', array('type' => 'int', 'not null' => TRUE, 'default' => 0)); + // Populate new field from objectid before changing objectid to varchar + $ret[] = update_sql('UPDATE {i18n_strings} SET objectindex = objectid'); + db_change_field($ret, 'i18n_strings', 'objectid', 'objectid', array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => '')); + // Add format field + db_add_field($ret, 'i18n_strings', 'format', array('type' => 'int', 'not null' => TRUE, 'default' => 0)); + // Change wrong default value for property (was 'default') + db_change_field($ret, 'i18n_strings', 'property', 'property', array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => '')); + } + return $ret; +} + +/** + * Rename status field so it doesn't clash with other l10n modules + */ +function i18nstrings_update_6006() { + $ret = array(); + if (!variable_get('i18nstrings_update_skip', 0)) { + db_change_field($ret, 'locales_target', 'status', 'i18n_status', array('type' => 'int', 'not null' => TRUE, 'default' => 0)); + } + return $ret; +} \ No newline at end of file diff --git a/sites/all/modules/i18n/i18nstrings/i18nstrings.module b/sites/all/modules/i18n/i18nstrings/i18nstrings.module new file mode 100644 index 0000000..71f7f20 --- /dev/null +++ b/sites/all/modules/i18n/i18nstrings/i18nstrings.module @@ -0,0 +1,1230 @@ +' . t('This module adds support for other modules to translate user defined strings. Depending on which modules you have enabled that use this feature you may see different text groups to translate.') .''; + $output .= '
' . t('This works differently to Drupal standard localization system: The strings will be translated from the default language (which may not be English), so changing the default language may cause all these translations to be broken.') . '
'; + $output .= ''. t('Read more on the Internationalization handbook: Translating user defined strings.') .'
'; + return $output; + + case 'admin/build/translate/refresh': + $output = ''. t('On this page you can refresh and update values for user defined strings.') .'
'; + $output .= ''. t('To search and translate strings, use the translation interface pages.', array('@translate-interface' => url('admin/build/translate'))) .'
'; + $output .= '' . t('Important: To configure which Input formats are safe for translation, visit the configure strings page before refreshing your strings.', array('@configure-strings' => url('admin/settings/language/configure/strings'))) . '
'; + return $output; + + case 'admin/settings/language': + $output = ''. t('Warning: Changing the default language may have unwanted effects on string translations. Read more about String translation', array('@i18nstrings-help' => url('admin/help/i18nstrings'))) .'
'; + return $output; + case 'admin/settings/language/configure/strings': + $output = '' . t('When translating user defined strings that have an Input format associated, translators will be able to edit the text before it is filtered which may be a security risk for some filters. An obvious example is when using the PHP filter but other filters may also be dangerous.') . '
'; + $output .= '' . t('As a general rule do not allow any filtered text to be translated unless the translators already have access to that Input format. However if you are doing all your translations through this site\'s translation UI or the Localization client, and never importing translations for other textgroups than default, filter access will be checked for translators on every translation page.') . '
'; + $output .= '' . t('Important: After disallowing some Input format, use the refresh strings page so forbidden strings are deleted and not allowed anymore for translators.', array('@refresh-strings' => url('admin/build/translate/refresh'))) . '
'; + return $output; + case 'admin/settings/filters': + return '' . t('After updating your Input formats do not forget to review the list of formats allowed for string translations on the configure translatable strings page.', array('@configure-strings' => url('admin/settings/language/configure/strings'))) . '
'; + } +} + +/** + * Implementation of hook_menu(). + */ +function i18nstrings_menu() { + $items['admin/build/translate/refresh'] = array( + 'title' => 'Refresh', + 'weight' => 20, + 'type' => MENU_LOCAL_TASK, + 'page callback' => 'i18nstrings_admin_refresh_page', + 'file' => 'i18nstrings.admin.inc', + 'access arguments' => array('translate interface'), + ); + // Direct copy of the Configure tab from locale module to + // make space for the "Localization sharing" tab below. + $items['admin/settings/language/configure/language'] = array( + 'title' => 'Language negotiation', + 'page callback' => 'locale_inc_callback', + 'page arguments' => array('drupal_get_form', 'locale_languages_configure_form'), + 'access arguments' => array('administer languages'), + 'weight' => -10, + 'type' => MENU_DEFAULT_LOCAL_TASK, + ); + $items['admin/settings/language/configure/strings'] = array( + 'title' => 'String translation', + 'weight' => 20, + 'type' => MENU_LOCAL_TASK, + 'page callback' => 'drupal_get_form', + 'page arguments' => array('i18nstrings_admin_settings'), + 'file' => 'i18nstrings.admin.inc', + 'access arguments' => array('administer filters'), + ); + + // AJAX callback path for strings. + $items['i18nstrings/save'] = array( + 'title' => 'Save string', + 'page callback' => 'i18nstrings_save_string', + 'access arguments' => array('use on-page translation'), + 'type' => MENU_CALLBACK, + ); + return $items; +} + +/** + * Implementation of hook_form_alter(). + * + * Add English language in some string forms when it is not the default. + */ +function i18nstrings_form_alter(&$form, $form_state, $form_id) { + switch ($form_id) { + case 'locale_translate_export_po_form': + case 'locale_translate_import_form': + $names = locale_language_list('name', TRUE); + if (language_default('language') != 'en' && array_key_exists('en', $names)) { + if (isset($form['export'])) { + $form['export']['langcode']['#options']['en'] = $names['en']; + } + else { + $form['import']['langcode']['#options'][t('Already added languages')]['en'] = $names['en']; + } + } + break; + + case 'locale_translate_edit_form': + // Restrict filter permissions and handle validation and submission for i18n strings + $context = db_fetch_object(db_query("SELECT * FROM {i18n_strings} WHERE lid = %d", $form['lid']['#value'])); + if ($context) { + $form['i18nstrings_context'] = array('#type' => 'value', '#value' => $context); + // Replace validate callback + $form['#validate'] = array('i18nstrings_translate_edit_form_validate'); + if ($context->format) { + $format = filter_formats($context->format); + $disabled = !filter_access($context->format); + if ($disabled) { + drupal_set_message(t('This string uses the %name input format. You are not allowed to translate or edit texts with this format.', array('%name' => $format->name)), 'warning'); + } + foreach (element_children($form['translations']) as $langcode) { + $form['translations'][$langcode]['#disabled'] = $disabled; + } + $form['translations']['format_help'] = array( + '#type' => 'item', + '#title' => t('Input format: @name', array('@name' => $format->name)), + '#value' => theme('filter_tips', _filter_tips($context->format, FALSE)) + ); + $form['submit']['#disabled'] = $disabled; + } + } + // Aditional submit callback + $form['#submit'][] = 'i18nstrings_translate_edit_form_submit'; + break; + case 'l10n_client_form': + $form['#action'] = url('i18nstrings/save'); + break; + } +} + +/** + * Process string editing form validations. + * + * If it is an allowed format, skip default validation, the text will be filtered later + */ +function i18nstrings_translate_edit_form_validate($form, &$form_state) { + $context = $form_state['values']['i18nstrings_context']; + if (empty($context->format)) { + // If not input format use regular validation for all strings + $copy_state = $form_state; + $copy_state['values']['textgroup'] = 'default'; + locale_translate_edit_form_validate($form, $copy_state); + } + elseif (!filter_access($context->format)) { + form_set_error('translations', t('You are not allowed to translate or edit texts with this input format.')); + } +} + +/** + * Process string editing form submissions. + * + * Mark translations as current. + */ +function i18nstrings_translate_edit_form_submit($form, &$form_state) { + $lid = $form_state['values']['lid']; + foreach ($form_state['values']['translations'] as $key => $value) { + if (!empty($value)) { + // An update has been made, so we assume the translation is now current. + db_query("UPDATE {locales_target} SET i18n_status = %d WHERE lid = %d AND language = '%s'", I18NSTRINGS_STATUS_CURRENT, $lid, $key); + } + } +} + +/** + * Check if translation is required for this language code. + * + * Translation is required when default language is different from the given + * language, or when default language translation is explicitly enabled. + * + * No UI is provided to enable translation of default language. On the other + * hand, you can enable/disable translation for a specific language by adding + * the following to your settings.php + * + * @code + * // Enable translation of specific language. Language code is 'xx' + * $conf['i18nstrings_translate_langcode_xx'] = TRUE; + * // Disable translation of specific language. Language code is 'yy' + * $conf['i18nstrings_translate_langcode_yy'] = FALSE; + * @endcode + */ +function i18nstrings_translate_langcode($langcode) { + static $translate = array(); + if (!isset($translate[$langcode])) { + $translate[$langcode] = variable_get('i18nstrings_translate_langcode_' . $langcode, language_default('language') != $langcode); + } + return $translate[$langcode]; +} + +/** + * Get configurable string. + * + * The difference with i18nstrings() is that it doesn't use a default string, it will be retrieved too. + * + * This is used for source texts that we don't have stored anywhere else. I.e. for the content + * types help text (i18ncontent module) there's no way we can override the default (configurable) help text + * so what we do is to make it blank in the configuration (so node module doesn't display it) + * and then we provide that help text for *all* languages, out from the locales tables. + * + * As the original language string will be stored in locales too so it should be only used when updating. + */ +function i18nstrings_ts($name, $string = '', $langcode = NULL, $update = FALSE) { + global $language; + + $langcode = $langcode ? $langcode : $language->language; + $translation = NULL; + + if ($update) { + i18nstrings_update_string($name, $string); + } + + $translation = i18nstrings_translate_string($name, $string, $langcode); + return $translation; +} + +/** + * Debug utility. Marks the translated strings. + */ +function _i18nstrings($name, $string, $langcode = NULL) { + $context = i18nstrings_context($name, $string); + $context = implode('/', (array)$context); + return i18nstrings($name, $string, $langcode) .'[T:'. $string .'('. $context .')]'; +} + +/** + * Get translation for user defined string. + * + * This function is intended to return translations for plain strings that have NO input format + * + * @param $name + * Textgroup and location glued with ':' + * @param $string + * String in default language + * @param $langcode + * Language code to get translation for + */ +function i18nstrings_translate_string($name, $string, $langcode) { + global $language; + + $context = i18nstrings_context($name, $string); + // Search for existing translation (result will be cached in this function call) + $translation = i18nstrings_get_string($context, $langcode); + // Add for l10n client if available + i18nstrings_add_l10n_client($langcode, $string, $translation, $context, FALSE); + + return $translation ? $translation : $string; +} + +/** + * Add string to l10n strings if enabled and allowed for this string + * + * @param $langcode + * Language code to translate to + * @param $string + * Original string to be translated (usually in default language) + * @param $translation + * Translated string if found + * @param $context + * Context object that must contain 'textgroup' property + * @param $source + * Source string object that must contain 'format' property + * FALSE for not checking the source format + */ +function i18nstrings_add_l10n_client($langcode, $string, $translation, $context, $source = NULL) { + global $language; + + // If current language add to l10n client list for later on page translation. + // If langcode translation was disabled we are not supossed to reach here. + if ($language->language == $langcode && function_exists('l10_client_add_string_to_page')) { + $translation = $translation ? $translation : TRUE; + if ($source === FALSE) { + // This means it is a plain string, we don't need to check the format + l10_client_add_string_to_page($string, $translation, $context->textgroup); + } + else { + // Additional checking for input format, if its a dangerous one we ignore the string + $source = $source ? $source : i18nstrings_get_source($context, $string); + if (!empty($source) && (i18nstrings_allowed_format($source->format) || filter_access($source->format))) { + l10_client_add_string_to_page($string, $translation, $context->textgroup); + } + } + } +} + +/** + * Translate object properties. + */ +function i18nstrings_translate_object($context, &$object, $properties = array(), $langcode = NULL) { + global $language; + + $langcode = $langcode ? $langcode : $language->language; + // If language is default, just return. + if (i18nstrings_translate_langcode($langcode)) { + $context = i18nstrings_context($context); + // @ TODO Object prefetch + foreach ($properties as $property) { + $context->property = $property; + $context->location = i18nstrings_location($context); + if (!empty($object->$property)) { + $object->$property = i18nstrings_translate_string($context, $object->$property, $langcode); + } + } + } +} + +/** + * Update / create object properties. + */ +function i18nstrings_update_object($context, $object, $properties = array()) { + $context = i18nstrings_context($context); + foreach ($properties as $property) { + $context->property = $property; + $context->location = i18nstrings_location($context); + if (!empty($object->$property)) { + i18nstrings_update_string($context, $object->$property); + } + } +} + +/** + * Update / create / remove string. + * + * @param $context + * String context. + * @pram $string + * New value of string for update/create. May be empty for removing. + * @param $format + * Input format, that must have been checked against allowed formats for translation + * @return status + * SAVED_UPDATED | SAVED_NEW | SAVED_DELETED + */ +function i18nstrings_update_string($context, $string, $format = 0) { + $context = i18nstrings_context($context, $string, $format); + + if ($string) { + return i18nstrings_add_string($context, $string, $format); + } + else { + return i18nstrings_remove_string($context); + } +} + +/** + * Update string translation. + */ +function i18nstrings_update_translation($context, $langcode, $translation) { + if ($source = i18nstrings_get_source($context, $translation)) { + db_query("INSERT INTO {locales_target} (lid, language, translation) VALUES(%d, '%s', '%s')", $source->lid, $langcode, $translation); + } +} + +/** + * Add source string to the locale tables for translation. + * + * It will also add data into i18n_strings table for faster retrieval and indexing of groups of strings. + * Some string context doesn't have a numeric oid (I.e. content types), it will be set to zero. + * + * This function checks for already existing string without context for this textgroup and updates it accordingly. + * It is intended for backwards compatibility, using already created strings. + * + * @param $name + * Textgroup and location glued with ':' + * @param $string + * Source string (string in default language) + * @param $format + * Input format, for strings that will go through some filter + * @return + * Update status. + */ +function i18nstrings_add_string($name, $string, $format = NULL) { + $context = i18nstrings_context($name, $string, $format); + $location = i18nstrings_location($context); + // Check if we have a source string. + $source = i18nstrings_get_source($context, $string); + // Default return status if nothing happens + $status = -1; + // The string may not be allowed for translation depending on its format. + if (isset($format) && !i18nstrings_allowed_format($format)) { + if ($source) { + // The format may have changed and it's not allowed now, delete the source string + return i18nstrings_remove_string($context); + } + else { + // We just don't do anything + return $status; + } + } + if ($source) { + if ($source->source != $string) { + // String has changed + db_query("UPDATE {locales_source} SET source = '%s', location = '%s' WHERE lid = %d", $string, $location, $source->lid); + db_query("UPDATE {locales_target} SET i18n_status = %d WHERE lid = %d", I18NSTRINGS_STATUS_UPDATE, $source->lid); + $status = SAVED_UPDATED; + } + elseif ($source->location != $location) { + // It's not changed but it didn't have location set + db_query("UPDATE {locales_source} SET location = '%s' WHERE lid = %d", $location, $source->lid); + $status = SAVED_UPDATED; + } + // Complete metadata. + $context->lid = $source->lid; + } + else { + db_query("INSERT INTO {locales_source} (location, source, textgroup, version) VALUES ('%s', '%s', '%s', '%s')", $location, $string, $context->textgroup, 1); + // Mysql just gets last id for latest query + $context->lid = db_last_insert_id('locales_source', 'lid'); + // Clear locale cache so this string can be added in a later request. + cache_clear_all('locale:'. $context->textgroup .':', 'cache', TRUE); + // Create string. + $status = SAVED_NEW; + } + // Update metadata + i18nstrings_save_context($context); + return $status; +} + +/** + * Check if input format is allowed for translation or whether a textgroup is 'safe'. + * + * @param $format + * Input format key or NULL if not format (will be allowed) + * @param $textgroup + * Check whether strings for this textgroup are allowed when no format information + */ +function i18nstrings_allowed_format($format = NULL, $textgroup = NULL) { + $allowed_formats = variable_get('i18nstrings_allowed_formats', array(variable_get('filter_default_format', 1))); + if (isset($format)) { + return in_array(filter_resolve_format($format), $allowed_formats); + } + elseif ($textgroup) { + $allowed_groups = variable_get('i18nstrings_allowed_textgroups', array()); + return (i18nstrings_group_info($textgroup, 'format') === FALSE) || in_array($textgroup, $allowed_groups); + } + else { + // No format, no textgroup, this is OK + return TRUE; + } +} + +/** + * Save / update context metadata. + * + * There seems to be a race condition sometimes so skip errors, #277711 + */ +function i18nstrings_save_context($context) { + if (db_result(db_query('SELECT lid FROM {i18n_strings} WHERE lid = %d', $context->lid))) { + @db_query("UPDATE {i18n_strings} SET type = '%s', objectid = '%s', objectindex = %d, property = '%s', format = %d WHERE lid = %d", $context->type, $context->objectid, (int)$context->objectid, $context->property, $context->format, $context->lid); + } + else { + @db_query("INSERT INTO {i18n_strings} (lid, type, objectid, objectindex, property, format) VALUES(%d, '%s', '%s', %d, '%s', %d)", $context->lid, $context->type, $context->objectid, (int)$context->objectid, $context->property, $context->format); + } +} + +/** + * Get source string provided a string context. + * + * This will search first with the full context parameters and, if not found, + * it will search again only with textgroup and source string. + * + * @param $context + * Context string or object. + * @return + * Context object if it exists. + */ +function i18nstrings_get_source($context, $string = NULL) { + $context = i18nstrings_context($context, $string); + // Check if we have the string for this location. + list($where, $args) = i18nstrings_context_query($context); + if ($source = db_fetch_object(db_query("SELECT s.*, i.type, i.objectid, i.property, i.format FROM {locales_source} s LEFT JOIN {i18n_strings} i ON s.lid = i.lid WHERE ". implode(' AND ', $where), $args))) { + $source->context = $context; + return $source; + } + // Search for the same string for this textgroup without object data. + if ($string && $source = db_fetch_object(db_query("SELECT s.*, i.type, i.objectid, i.property, i.format FROM {locales_source} s LEFT JOIN {i18n_strings} i ON s.lid = i.lid WHERE s.textgroup = '%s' AND s.source = '%s' AND i.lid IS NULL", $context->textgroup, $string))) { + $source->context = NULL; + return $source; + } +} + +/** + * Get string for a language. + * + * @param $context + * Context string or object. + * @param $langcode + * Language code to retrieve string for. + * + * @return + * - Translation string as object if found. + * - FALSE if no translation + * + */ +function i18nstrings_get_string($context, $langcode) { + $context = i18nstrings_context($context); + // First try the cache + $translation = i18nstrings_cache($context, $langcode); + if (isset($translation)) { + return $translation; + } + else { + // Search translation and add it to the cache. + list($where, $args) = i18nstrings_context_query($context); + $where[] = "t.language = '%s'"; + $args[] = $langcode; + // Get translation that may have an input format to apply + $text = db_fetch_object(db_query("SELECT s.lid, t.translation FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid WHERE ". implode(' AND ', $where), $args)); + if ($text && $text->translation) { + i18nstrings_cache($context, $langcode, NULL, $text->translation); + return $text->translation; + } + else { + i18nstrings_cache($context, $langcode, NULL, FALSE); + return $text ? NULL : FALSE ; + } + } +} + +/** + * Get translation from the database. Full object with input format. + * + * This one doesn't return anything if we don't have the full i18n strings data there + * to prevent missing data resulting in missing input formats + */ +function i18nstrings_get_translation($context, $langcode) { + $context = i18nstrings_context($context); + list($where, $args) = i18nstrings_context_query($context); + $where[] = "t.language = '%s'"; + $args[] = $langcode; + return db_fetch_object(db_query("SELECT s.lid, t.translation, i.format FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid INNER JOIN {i18n_strings} i ON s.lid = i.lid WHERE ". implode(' AND ', $where), $args)); +} + +/** + * Remove string for a given context. + */ +function i18nstrings_remove_string($context, $string = NULL) { + $context = i18nstrings_context($context, $string); + if ($source = i18nstrings_get_source($context, $string)) { + db_query("DELETE FROM {locales_target} WHERE lid = %d", $source->lid); + db_query("DELETE FROM {i18n_strings} WHERE lid = %d", $source->lid); + db_query("DELETE FROM {locales_source} WHERE lid = %d", $source->lid); + cache_clear_all('locale:'. $context->textgroup .':', 'cache', TRUE); + return SAVED_DELETED; + } +} + +/** + * Remove a string translation for a given context and language. + */ +function i18nstrings_remove_translation($context, $langcode) { + $context = i18nstrings_context($context); + if ($source = i18nstrings_get_source($context)) { + db_query("DELETE FROM {locales_target} WHERE lid = %d AND language = '%s'", $source->lid, $langcode); + } +} + +/** + * Update context for strings. + * + * As some string locations depend on configurable values, the field needs sometimes to be updated + * without losing existing translations. I.e: + * - profile fields indexed by field name. + * - content types indexted by low level content type name. + * + * Example: + * 'profile:field:oldfield:*' -> 'profile:field:newfield:*' + */ +function i18nstrings_update_context($oldname, $newname) { + // Get context replacing '*' with empty string. + $oldcontext = i18nstrings_context(str_replace('*', '', $oldname)); + $newcontext = i18nstrings_context(str_replace('*', '', $newname)); + + // Get location with placeholders. + $location = i18nstrings_location(str_replace('*', '%', $oldname)); + foreach (array('textgroup', 'type', 'objectid', 'property') as $field) { + if ((!empty($oldcontext->$field) || !empty($newcontext->$field)) && $oldcontext->$field != $newcontext->$field) { + $replace[$field] = $newcontext->$field; + } + } + + // Query and replace if there are any fields. It is possible that under some circumstances fields are the same + if (!empty($replace)) { + $result = db_query("SELECT s.*, i.type, i.objectid, i.property FROM {locales_source} s LEFT JOIN {i18n_strings} i ON s.lid = i.lid WHERE s.textgroup = '%s' AND s.location LIKE '%s'", $oldcontext->textgroup, $location); + while ($source = db_fetch_object($result)) { + // Make sure we have string and context. + $context = i18nstrings_context($oldcontext->textgroup .':'. $source->location); + foreach ($replace as $field => $value) { + $context->$field = $value; + } + // Update source string. + db_query("UPDATE {locales_source} SET textgroup = '%s', location = '%s' WHERE lid = %d", $context->textgroup, i18nstrings_location($context), $source->lid); + // Update object data. + db_query("UPDATE {i18n_strings} SET type = '%s', objectid = '%s', property = '%s' WHERE lid = %d", $context->type, $context->objectid, $context->property, $source->lid); + } + drupal_set_message(t('Updating string names from %oldname to %newname.', array('%oldname' => $oldname, '%newname' => $newname))); + } +} + +/** + * Provides interface translation services. + * + * This function is called from i18nstrings() to translate a string if needed. + * + * @param $textgroup + * + * @param $string + * A string to look up translation for. If omitted, all the + * cached strings will be returned in all languages already + * used on the page. + * @param $langcode + * Language code to use for the lookup. + */ +function i18nstrings_textgroup($textgroup, $string = NULL, $langcode = NULL) { + global $language; + static $locale_t; + + // Return all cached strings if no string was specified. + if (!isset($string)) { + return isset($locale_t[$textgroup]) ? $locale_t[$textgroup] : array(); + } + + $langcode = isset($langcode) ? $langcode : $language->language; + + // Store database cached translations in a static variable. + if (!isset($locale_t[$langcode])) { + $locale_t[$langcode] = array(); + // Disabling the usage of string caching allows a module to watch for + // the exact list of strings used on a page. From a performance + // perspective that is a really bad idea, so we have no user + // interface for this. Be careful when turning this option off! + if (variable_get('locale_cache_strings', 1) == 1) { + if ($cache = cache_get('locale:'. $textgroup .':'. $langcode, 'cache')) { + $locale_t[$textgroup][$langcode] = $cache->data; + } + else { + // Refresh database stored cache of translations for given language. + // We only store short strings used in current version, to improve + // performance and consume less memory. + $result = db_query("SELECT s.source, t.translation, t.language FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = '%s' WHERE s.textgroup = '%s' AND s.version = '%s' AND LENGTH(s.source) < 75", $langcode, $textgroup, VERSION); + while ($data = db_fetch_object($result)) { + $locale_t[$textgroup][$langcode][$data->source] = (empty($data->translation) ? TRUE : $data->translation); + } + cache_set('locale:'. $textgroup .':'. $langcode, $locale_t[$textgroup][$langcode]); + } + } + } + + // If we have the translation cached, skip checking the database + if (!isset($locale_t[$textgroup][$langcode][$string])) { + + // We do not have this translation cached, so get it from the DB. + $translation = db_fetch_object(db_query("SELECT s.lid, t.translation, s.version FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = '%s' WHERE s.source = '%s' AND s.textgroup = '%s'", $langcode, $string, $textgroup)); + if ($translation) { + // We have the source string at least. + // Cache translation string or TRUE if no translation exists. + $locale_t[$textgroup][$langcode][$string] = (empty($translation->translation) ? TRUE : $translation->translation); + + if ($translation->version != VERSION) { + // This is the first use of this string under current Drupal version. Save version + // and clear cache, to include the string into caching next time. Saved version is + // also a string-history information for later pruning of the tables. + db_query_range("UPDATE {locales_source} SET version = '%s' WHERE lid = %d", VERSION, $translation->lid, 0, 1); + cache_clear_all('locale:'. $textgroup .':', 'cache', TRUE); + } + } + else { + // We don't have the source string, cache this as untranslated. + db_query("INSERT INTO {locales_source} (location, source, textgroup, version) VALUES ('%s', '%s', '%s', '%s')", request_uri(), $string, $textgroup, VERSION); + $locale_t[$langcode][$string] = TRUE; + // Clear locale cache so this string can be added in a later request. + cache_clear_all('locale:'. $textgroup .':', 'cache', TRUE); + } + } + + return ($locale_t[$textgroup][$langcode][$string] === TRUE ? $string : $locale_t[$textgroup][$langcode][$string]); +} + +/** + * Convert context string in a context object. + * + * Example: + * 'taxonomy:term:1:name' + * + * will become a $context object where + * $context->textgroup = 'taxonomy'; + * $context->type = 'term'; + * $context->objectid = 1; + * $context->property = 'name'; + * + * Examples: + * 'taxonomy:title' -> (taxonomy, title, 0, 0) + * 'nodetype:type:[type]:name' + * 'nodetype:type:[type]:description' + * 'profile:category' + * 'profile:field:[fid]:title' + * + * When we don't have 'objectid' or 'property', like for 'profile:category' we need to use + * the string itself as a search key, so we store it in $context->source + * + * If the name has more than 4 elements glued by ':' we add the remaining ones into property + * + * @param $context + * Context string or object. + * @param $string + * For some textgroups and objects that don't have ids we use the string itself as index. + * @return + * Context object with textgroup, type, objectid, property and location names. + */ +function i18nstrings_context($context, $string = NULL, $format = 0) { + // Context may be already an object. + if (is_object($context)) { + return $context; + } + else { + // Split the name in four parts, remaining elements will be in the last one + $parts = explode(':', $context); + $context = new Stdclass(); + $context->textgroup = array_shift($parts); + $context->type = array_shift($parts); + $context->objectid = $parts ? array_shift($parts) : ''; + // Remaining elements glued again with ':' + $context->property = $parts ? implode(':', $parts) : ''; + $context->format = $format; + $context->location = i18nstrings_location($context); + // The value may be zero so we check first with is_numeric() + if (!is_numeric($context->objectid) && !$context->objectid && !$context->property && $string) { + $context->source = $string; + } + return $context; + } +} + +/** + * Get message parameters from context and string. + */ +function i18nstrings_params($context, $string = NULL) { + if (!empty($context)) { + return array( + '%location' => i18nstrings_location($context), + '%textgroup' => isset($context->textgroup) ? $context->textgroup : '', + '%string' => !empty($string) ? $string : t('[empty string]'), + ); + } +} + +/** + * Get query conditions for this context. + */ +function i18nstrings_context_query($context, $alias = 's') { + $where = array("$alias.textgroup = '%s'", "$alias.location = '%s'"); + $args = array($context->textgroup, $context->location); + if (!empty($context->source)) { + $where[] = "s.source = '%s'"; + $args[] = $context->source; + } + return array($where, $args); +} + +/** + * Get location string from context. + * + * Returns the location for the locale table for a string context. + */ +function i18nstrings_location($context) { + if (is_string($context)) { + $context = i18nstrings_context($context); + } + $location[] = !empty($context->type) ? $context->type : ''; + // The value may be zero so we check first with is_numeric() + if (isset($context->objectid) && (is_numeric($context->objectid) || !empty($context->objectid))) { + $location[] = $context->objectid; + if (!empty($context->property)) { + $location[] = $context->property; + } + } + return implode(':', $location); +} + +/** + * Prefetch a number of object strings. + */ +function i18nstrings_prefetch($context, $langcode = NULL, $join = array(), $conditions = array()) { + global $language; + + $langcode = $langcode ? $langcode : $language->language; + // Add language condition. + $conditions['t.language'] = $langcode; + // Get context conditions. + $context = (array)i18nstrings_context($context); + foreach ($context as $key => $value) { + if ($value) { + if ($key == 'textgroup') { + $conditions['s.textgroup'] = $value; + } + else { + $conditions['i.'. $key] = $value; + } + } + } + // Prepare where clause + $where = $params = array(); + foreach ($conditions as $key => $value) { + if (is_array($value)) { + $where[] = $key .' IN ('. db_placeholders($value, is_int($value[0]) ? 'int' : 'string') .')'; + $params = array_merge($params, $value); + } + else { + $where[] = $key .' = '. is_int($value) ? '%d' : "'%s'"; + $params[] = $value; + } + } + $sql = "SELECT s.textgroup, s.source, i.type, i.objectid, i.property, t.translation FROM {locales_source} s"; + $sql .=" INNER JOIN {i18n_strings} i ON s.lid = i.lid INNER JOIN {locales_target} t ON s.lid = t.lid "; + $sql .= implode(' ', $join) .' '. implode(' AND ', $where); + $result = db_query($sql, $params); + + // Fetch all rows and store in cache. + while ($t = db_fetch_object($result)) { + i18nstrings_cache($t, $langcode, $t->source, $t->translation); + } + +} + +/** + * Retrieves and stores translations in page (static variable) cache. + * + * @param $context + * String id or context object + * @param $langcode + * Language code to translate to + * @param $string + * Source string when available + * @param $translation + * Translated string to store into the cache + * + * @return + * - Translation if chached (may be false if no translation) + * - NULL if no value cached + */ +function i18nstrings_cache($context, $langcode, $string = NULL, $translation = NULL) { + static $strings; + + $context = i18nstrings_context($context, $string); + + if (!$context->objectid && $context->source) { + // This is a type indexed by string. + $context->objectid = $context->source; + } + // At this point context must have at least textgroup and type. + if (isset($translation)) { + if ($context->property) { + $strings[$langcode][$context->textgroup][$context->type][$context->objectid][$context->property] = $translation; + } + elseif ($context->objectid) { + $strings[$langcode][$context->textgroup][$context->type][$context->objectid] = $translation; + } + else { + $strings[$langcode][$context->textgroup][$context->type] = $translation; + } + } + else { + // Search up the tree for the object or a default. + $search = &$strings[$langcode]; + $default = NULL; + $list = array('textgroup', 'type', 'objectid', 'property'); + while (($field = array_shift($list)) && !empty($context->$field)) { + if (isset($search[$context->$field])) { + $search = &$search[$context->$field]; + if (isset($search['#default'])) { + $default = $search['#default']; + } + } + else { + // We dont have cached this tree so we return the default. + return $default; + } + } + // Returns the part of the array we got to. + return $search; + } + +} + +/** + * Callback for menu title translation. + * + * @param $name + * String id + * @param $string + * Default string, title in default language + * @param $callback + * Aditional callback to be run after this one + */ +function i18nstrings_title_callback($name, $string, $callback = NULL) { + $string = i18nstrings($name, $string); + if ($callback) { + $string = $callback($string); + } + return $string; +} + +/** + * Refresh all user defined strings for a given text group. + * + * @param $group + * Text group to refresh + * @param $delete + * Optional, delete existing (but not refresed, strings and translations) + * @return Boolean + * True if the strings have been refreshed successfully. False otherwise. + */ +function i18nstrings_refresh_group($group, $delete = FALSE) { + // Check for the refresh callback + $refresh_callback = i18nstrings_group_info($group, 'refresh callback'); + if (!$refresh_callback) { + return FALSE; + } + // Delete data from i18n_strings so it is recreated + db_query("DELETE FROM {i18n_strings} WHERE lid IN (SELECT lid FROM {locales_source} WHERE textgroup = '%s')", $group); + + $result = call_user_func($refresh_callback); + + // Now delete all source strings that were not refreshed + if ($result && $delete) { + $result = db_query("SELECT s.* FROM {locales_source} s LEFT JOIN {i18n_strings} i ON s.lid = i.lid WHERE s.textgroup = '%s' AND i.lid IS NULL", $group); + while ($source = db_fetch_object($result)) { + db_query("DELETE FROM {locales_target} WHERE lid = %d", $source->lid); + db_query("DELETE FROM {locales_source} WHERE lid = %d", $source->lid); + } + } + + cache_clear_all('locale:'. $group .':', 'cache', TRUE); + return $result; +} + +/** + * Get refresh callback for a text group. + * + * @param $group + * + * @return callback + */ +function i18nstrings_group_info($group = NULL, $property = NULL) { + static $info; + + if (!isset($info)) { + $info = module_invoke_all('locale', 'info'); + } + + if ($group && $property) { + return isset($info[$group][$property]) ? $info[$group][$property] : NULL; + } + elseif ($group) { + return isset($info[$group]) ? $info[$group] : array(); + } + else { + return $info; + } +} + +/*** l10n client related functions ***/ + +/** + * Menu callback. Saves a string translation coming as POST data. + */ +function i18nstrings_save_string() { + global $user, $language; + + if (user_access('use on-page translation')) { + $textgroup = !empty($_POST['textgroup']) ? $_POST['textgroup'] : 'default'; + // Default textgroup will be handled by l10n_client module + if ($textgroup == 'default') { + l10n_client_save_string(); + } + elseif (isset($_POST['source']) && isset($_POST['target']) && !empty($_POST['form_token']) && drupal_valid_token($_POST['form_token'], 'l10n_client_form')) { + i18nstrings_save_translation($language->language, $_POST['source'], $_POST['target'], $textgroup); + } + } +} + +/** + * Import translation for a given textgroup. + * + * @TODO Check string format properly + * + * This will update multiple strings if there are duplicated ones + * + * @param $langcode + * Language code to import string into. + * @param $source + * Source string. + * @param $translation + * Translation to language specified in $langcode. + * @param $plid + * Optional plural ID to use. + * @param $plural + * Optional plural value to use. + * @return + * The number of strings updated + */ +function i18nstrings_save_translation($langcode, $source, $translation, $textgroup) { + include_once 'includes/locale.inc'; + + $result = db_query("SELECT s.lid, i.format FROM {locales_source} s LEFT JOIN {i18n_strings} i ON s.lid = i.lid WHERE s.source = '%s' AND s.textgroup = '%s'", $source, $textgroup); + $count = 0; + while ($source = db_fetch_object($result)) { + // If we have a format, check format access. Otherwise do regular check. + if ($source->format ? filter_access($source->format) : locale_string_is_safe($translation)) { + $exists = (bool) db_result(db_query("SELECT lid FROM {locales_target} WHERE lid = %d AND language = '%s'", $source->lid, $langcode)); + if (!$exists) { + // No translation in this language. + db_query("INSERT INTO {locales_target} (lid, language, translation) VALUES (%d, '%s', '%s')", $source->lid, $langcode, $translation); + } + else { + // Translation exists, overwrite + db_query("UPDATE {locales_target} SET translation = '%s' WHERE language = '%s' AND lid = %d", $translation, $langcode, $source->lid); + } + $count ++; + } + } + return $count; +} + +/** + * @ingroup i18napi + * @{ + */ + +/** + * Translate or update user defined string. + * + * DEPRECATED, just kept for backwards compatibility. + * + * @todo Remove tt() for Drupal 7. + * @see i18nstrings() + */ +function tt($name, $string, $langcode = NULL) { + return i18nstrings($name, $string, $langcode); +} + +/** + * Translate user defined string. + * + * @param $name + * Textgroup and location glued with ':'. + * @param $string + * String in default language. Default language may or may not be English. + * @param $langcode + * Optional language code if different from current request language. + * + * @return $string + * Translated string, $string if not found + */ +function i18nstrings($name, $string, $langcode = NULL) { + global $language; + $langcode = $langcode ? $langcode : $language->language; + // If language is default, just return + if (i18nstrings_translate_langcode($langcode)) { + return i18nstrings_translate_string($name, $string, $langcode); + } + return $string; +} + +/** + * Get filtered translation. + * + * This function is intended to return translations for strings that have an input format + * + * @param $name + * Full string id + * @param $default + * Default string to return if not found, already filtered + * @param $langcode + * Optional language code if different from current request language. + */ +function i18nstrings_text($name, $default, $langcode = NULL) { + global $language; + $langcode = $langcode ? $langcode : $language->language; + + $context = i18nstrings_context($name, $default); + + // If language is default or we don't have translation, just return default string + if (i18nstrings_translate_langcode($langcode) && ($translation = i18nstrings_get_translation($name, $langcode))) { + $translated = check_markup($translation->translation, $translation->format, FALSE); + // Add for l10n client if available, we pass translation object that contains the format + i18nstrings_add_l10n_client($langcode, $default, $translated, $context, $translation); + } + else { + $translated = $default; + // Add for l10n client if available + i18nstrings_add_l10n_client($langcode, $default, $translated, $context); + } + + return $translated; +} + +/** + * Translation for plain string. In case it finds a translation it applies check_plain() to it + * + * @param $name + * Full string id + * @param $default + * Default string to return if not found + * @param $langcode + * Optional language code if different from current request language. + * @param $filter_default + * Whether to filter (check_plain) the default too if it is retrieved + */ +function i18nstrings_string($name, $default, $langcode = NULL, $filter_default = FALSE) { + $translation = i18nstrings($name, NULL, $langcode); + if (isset($translation)) { + return check_plain($translation); + } + else { + return $filter_default ? check_plain($default) : $default; + } +} + +/** + * Update / create translation source for user defined strings. + * + * @param $name + * Textgroup and location glued with ':'. + * @param $string + * Source string in default language. Default language may or may not be English. + * @param $format + * Input format when the string is diplayed through input formats + */ +function i18nstrings_update($name, $string, $format = NULL) { + $context = i18nstrings_context($name, $string, $format); + $params = i18nstrings_params($context, $string); + if (!i18nstrings_allowed_format($format)) { + // This format is not allowed, so we remove the string, in this case we produce a warning + drupal_set_message(t('The string %location for textgroup %textgroup is not allowed for translation because of its input format.', $params), 'warning'); + return i18nstrings_remove_string($context, $string); + } + $status = i18nstrings_update_string($context, $string, $format); + // Log status message + switch ($status) { + case SAVED_UPDATED: + watchdog('i18nstrings', 'Updated string %location for textgroup %textgroup: %string', $params); + break; + case SAVED_NEW: + watchdog('i18nstrings', 'Created string %location for text group %textgroup: %string', $params); + break; + } + return $status; +} + +/** + * Remove source and translations for user defined string. + * + * Though for most strings the 'name' or 'string id' uniquely identifies that string, + * there are some exceptions (like profile categories) for which we need to use the + * source string itself as a search key. + * + * @param $name + * Textgroup and location glued with ':'. + * @param $string + * Optional source string (string in default language). + */ +function i18nstrings_remove($name, $string = NULL) { + $status = i18nstrings_remove_string($name, $string); + // Log status message + $context = i18nstrings_context($name, $string); + $params = i18nstrings_params($context, $string); + switch ($status) { + case SAVED_DELETED; + watchdog('i18nstrings', 'Deleted string %location for textgroup %textgroup: %string', $params); + } + return $status; +} + +/** + * @} End of "ingroup i18napi". + */ diff --git a/sites/all/modules/i18n/i18nsync/README.txt b/sites/all/modules/i18n/i18nsync/README.txt new file mode 100644 index 0000000..4badefa --- /dev/null +++ b/sites/all/modules/i18n/i18nsync/README.txt @@ -0,0 +1,22 @@ + +README.txt +========== +Drupal module: i18nsync (Synchronization) + +This module will handle content synchronization accross 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: + +// Available fields for synchronization, for all node types. +$conf['i18nsync_fields_node'] = array( + 'field1' => t('Field 1 name'), + 'field2' => t('Field 2 name'), + ... +); + +// More fields for a specific content type 'nodetype' only. +$conf['i18nsync_fields_node_nodetype'] = array( + 'field3' => t('Field 3 name'), + ... +); \ No newline at end of file diff --git a/sites/all/modules/i18n/i18nsync/i18nsync.info b/sites/all/modules/i18n/i18nsync/i18nsync.info new file mode 100644 index 0000000..c8b3620 --- /dev/null +++ b/sites/all/modules/i18n/i18nsync/i18nsync.info @@ -0,0 +1,11 @@ +name = Synchronize translations +description = Synchronizes taxonomy and fields accross translations of the same content. +dependencies[] = i18n +package = Multilanguage +core = 6.x +; Information added by drupal.org packaging script on 2011-10-11 +version = "6.x-1.10" +core = "6.x" +project = "i18n" +datestamp = "1318336004" + diff --git a/sites/all/modules/i18n/i18nsync/i18nsync.install b/sites/all/modules/i18n/i18nsync/i18nsync.install new file mode 100644 index 0000000..73d40d2 --- /dev/null +++ b/sites/all/modules/i18n/i18nsync/i18nsync.install @@ -0,0 +1,16 @@ +'. t('This module synchronizes content taxonomy and fields accross translations:') .''; + $output .= ''. 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:') .'
'; + $output .= ''. t('Note that permissions are not checked for each node. So if someone can edit a node and it is set to synchronize, all the translations will be synchronized anyway.') .'
'; + $output .= ''. t('To enable synchronization check content type options to select which fields to synchronize for each node type.') .'
'; + $output .= ''. t('The list of available fields for synchronization will include some standard node fields and all CCK fields. You can add more fields to the list in a configuration variable. See README.txt for how to do it.') .'
'; + $output .= ''. t('For more information, see the online handbook entry for Internationalization module.', array('@i18n' => 'http://drupal.org/node/133977')) .'
'; + return $output; + } +} + +/** + * Implementation of hook_theme(). + */ +function i18nsync_theme() { + return array( + 'i18nsync_workflow_checkbox' => array( + 'arguments' => array('item' => NULL), + ), + ); +} + +/** + * Implementation of hook_form_alter(). + * - Vocabulary options + * - Content type options + */ +function i18nsync_form_alter(&$form, $form_state, $form_id) { + // Taxonomy vocabulary form. + switch ($form_id) { + case 'node_type_form': + $type = $form['#node_type']->type; + $current = i18nsync_node_fields($type); + $disabled = $form['i18n']['#disabled']; + $form['i18n']['i18nsync_nodeapi'] = array( + '#type' => 'fieldset', + '#tree' => TRUE, + '#title' => t('Synchronize translations'), + '#collapsible' => TRUE, + '#collapsed' => !count($current), + '#description' => t('Select which fields to synchronize for all translations of this content type.'), + '#disabled' => $disabled, + ); + // Each set provides title and options. We build a big checkboxes control for it to be + // saved as an array. Special themeing for group titles. + foreach (i18nsync_node_available_fields($type) as $group => $data) { + $title = $data['#title']; + if (!empty($data['#options'])) { + foreach ($data['#options'] as $field => $name) { + $form['i18n']['i18nsync_nodeapi'][$field] = array( + '#group_title' => $title, + '#title' => $name, + '#type' => 'checkbox', + '#default_value' => in_array($field, $current), + '#theme' => 'i18nsync_workflow_checkbox', + '#disabled' => $disabled, + ); + $title = ''; + } + } + } + break; + case 'node_delete_confirm': + // Intercept form submission so we can handle uploads, replace callback + $form['#submit'] = array_merge(array('i18nsync_node_delete_submit'), $form['#submit']); + break; + case 'node_admin_content': + if (!empty($form['operation']) && $form['operation']['#value'] == 'delete') { + $form['#submit'] = array_merge(array('i18nsync_node_delete_submit'), $form['#submit']); + } + break; + } +} + +/** + * Submit callback for + * - node delete confirm + * - node multiple delete confirm + */ +function i18nsync_node_delete_submit($form, $form_state) { + if ($form_state['values']['confirm']) { + if (!empty($form_state['values']['nid'])) { + // Single node + i18nsync_node_delete_prepare($form_state['values']['nid']); + } + elseif (!empty($form_state['values']['nodes'])) { + // Multiple nodes + foreach ($form_state['values']['nodes'] as $nid => $value) { + i18nsync_node_delete_prepare($nid); + } + } + } + // Then it will go through normal form submission +} + +/** + * Prepare node for deletion, work out synchronization issues + */ +function i18nsync_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); + } + } +} + +/** + * Theming function for workflow checkboxes. + */ +function theme_i18nsync_workflow_checkbox($element) { + $output = $element['#group_title'] ? ''. t('To search and translate strings, use the translation interface pages.', array('@translate-interface' => url('admin/build/translate'))) .'
'; + $output .= ''. t('For more information, see the online handbook entry for Internationalization module.', array('@i18n' => 'http://drupal.org/node/133977')) .'
'; + return $output; + + case 'admin/settings/language/i18n': + $output = ''. t('%capital_name is a localizable vocabulary. You will be able to translate term names and descriptions using the translate interface pages.', array('%capital_name' => drupal_ucfirst($vocabulary->name), '%name' => $vocabulary->name, '@translate-interface' => url('admin/build/translate'))) .'
'; + + case I18N_TAXONOMY_LANGUAGE: + return ''. t('%capital_name is a vocabulary with a fixed language. All the terms in this vocabulary will have %language language.', array('%capital_name' => drupal_ucfirst($vocabulary->name), '%name' => $vocabulary->name, '%language' => i18n_language_property($vocabulary->language, 'name'))) .'
'; + + case I18N_TAXONOMY_TRANSLATE: + return ''. t('%capital_name is a full multilingual vocabulary. You will be able to set a language for each term and create translation relationships.', array('%capital_name' => drupal_ucfirst($vocabulary->name))) .'
'; + } + + } +} + +/** + * Returns list of vocabulary modes. + */ +function _i18ntaxonomy_vocabulary_options() { + return array( + I18N_TAXONOMY_NONE => t('None. No multilingual options for this vocabulary.'), + I18N_TAXONOMY_LOCALIZE => t('Localize terms. Terms are common for all languages, but their name and description may be localized.'), + I18N_TAXONOMY_TRANSLATE => t('Per language terms. Different terms will be allowed for each language and they can be translated.'), + I18N_TAXONOMY_LANGUAGE => t('Set language to vocabulary. The vocabulary will have a global language and it will only show up for pages in that language.'), + ); +} + +/** + * Implementation of hook_menu(). + */ +function i18ntaxonomy_menu() { + $items['admin/content/taxonomy/%taxonomy_vocabulary/translation'] = array( + 'title' => 'Translation', + 'page callback' => 'i18ntaxonomy_page_vocabulary', + 'page arguments' => array(3, 5, 6), + 'access callback' => '_i18ntaxonomy_translation_tab', + 'access arguments' => array(3), + 'type' => MENU_LOCAL_TASK, + 'parent' => 'admin/content/taxonomy/%taxonomy_vocabulary', + 'file' => 'i18ntaxonomy.admin.inc', + ); + return $items; +} + +/** + * Implementation of hook_menu_alter(). + * + * Take over the taxonomy pages + */ +function i18ntaxonomy_menu_alter(&$items) { + // If ctool's page manager is active for the path skip this modules override. + if (variable_get('page_manager_term_view_disabled', TRUE)) { + // Taxonomy term page. Localize terms. + $items['taxonomy/term/%']['module'] = 'i18ntaxonomy'; + $items['taxonomy/term/%']['page callback'] = 'i18ntaxonomy_term_page'; + $items['taxonomy/term/%']['file'] = 'i18ntaxonomy.pages.inc'; + } + + // Localize autocomplete + $items['taxonomy/autocomplete']['module'] = 'i18ntaxonomy'; + $items['taxonomy/autocomplete']['page callback'] = 'i18ntaxonomy_autocomplete'; + $items['taxonomy/autocomplete']['file'] = 'i18ntaxonomy.pages.inc'; +} + +/** + * Menu access callback. Show tab only for full multilingual vocabularies. + */ +function _i18ntaxonomy_translation_tab($vocabulary) { + return i18ntaxonomy_vocabulary($vocabulary->vid) == I18N_TAXONOMY_TRANSLATE; +} + +/** + * Implementation of hook_locale(). + */ +function i18ntaxonomy_locale($op = 'groups', $group = NULL) { + switch ($op) { + case 'groups': + return array('taxonomy' => t('Taxonomy')); + case 'info': + $info['taxonomy']['refresh callback'] = 'i18ntaxonomy_locale_refresh'; + $info['taxonomy']['format'] = FALSE; + return $info; + } +} + +/** + * Refresh strings. + */ +function i18ntaxonomy_locale_refresh() { + foreach (taxonomy_get_vocabularies() as $vid => $vocabulary) { + if (empty($vocabulary->language)) { + i18nstrings_update("taxonomy:vocabulary:$vid:name", $vocabulary->name); + if ($vocabulary->help) { + i18nstrings_update("taxonomy:vocabulary:$vid:help", $vocabulary->help); + } + } + if (i18ntaxonomy_vocabulary($vid) == I18N_TAXONOMY_LOCALIZE) { + foreach (taxonomy_get_tree($vid, 0) as $term) { + i18nstrings_update("taxonomy:term:$term->tid:name", $term->name); + if ($term->description) { + i18nstrings_update("taxonomy:term:$term->tid:description", $term->description); + } + } + } + } + return TRUE; // Meaning it completed with no issues +} + +/** + * Implementation of hook_alter_translation_link(). + * + * Replaces links with pointers to translated versions of the content. + */ +function i18ntaxonomy_translation_link_alter(&$links, $path) { + if (preg_match("/^(taxonomy\/term\/)([^\/]*)(.*)$/", $path, $matches)) { //or at a taxonomy-listing? + foreach ($links as $langcode => $link) { + if ($str_tids = i18ntaxonomy_translation_tids($matches[2], $langcode)) { + $links[$langcode]['href'] = "taxonomy/term/$str_tids". $matches[3]; + } + } + } +} + +/** + * Implementation of hook_theme(). + */ +function i18ntaxonomy_theme() { + return array( + 'i18ntaxonomy_term_page' => array( + 'arguments' => array('tids' => array(), 'result' => NULL), + 'file' => 'i18ntaxonomy.pages.inc', + ), + ); +} + +/** + * Translate term name + * + * @param $tid + * Term id or term object + * @param $name + * Filtered default(untranslated) name + */ +function i18ntaxonomy_translate_term_name($tid, $name = '', $langcode = NULL) { + // If it is a term object we check for vocabulary options + if (is_object($tid)) { + return i18ntaxonomy_vocabulary($tid->vid) == I18N_TAXONOMY_LOCALIZE ? i18nstrings_string("taxonomy:term:$tid->tid:name", $tid->name, $langcode, TRUE) : check_plain($tid->name); + } + else { + return i18nstrings_string("taxonomy:term:$tid:name", $name, $langcode); + } +} + +/** + * Translate vocabulary name + * + * @param $vid + * Vocabulary id or vocabulary object + * @param $name + * Filtered default(untranslated) name + */ +function i18ntaxonomy_translate_vocabulary_name($vid, $name = '', $langcode = NULL) { + return is_object($vid) ? i18nstrings_string("taxonomy:vocabulary:$vid->vid:name", $vid->name, $langcode, TRUE) : i18nstrings_string("taxonomy:vocabulary:$vid:name", $name, $langcode); +} + +/** + * Get translated term's tid. + * + * @param $tid + * Node nid to search for translation. + * @param $language + * Language to search for the translation, defaults to current language. + * @param $default + * Value that will be returned if no translation is found. + * @return + * Translated term tid if exists, or $default. + */ +function i18ntaxonomy_translation_term_tid($tid, $language = NULL, $default = NULL) { + $translation = db_result(db_query("SELECT t.tid FROM {term_data} t INNER JOIN {term_data} a ON t.trid = a.trid AND t.tid <> a.tid WHERE a.tid = %d AND t.language = '%s' AND t.trid > 0", $tid, $language ? $language : i18n_get_lang())); + return $translation ? $translation : $default; +} + +/** + * Returns an url for the translated taxonomy-page, if exists. + */ +function i18ntaxonomy_translation_tids($str_tids, $lang) { + if (preg_match('/^([0-9]+[+ ])+[0-9]+$/', $str_tids)) { + $separator = '+'; + // The '+' character in a query string may be parsed as ' '. + $tids = preg_split('/[+ ]/', $str_tids); + } + elseif (preg_match('/^([0-9]+,)*[0-9]+$/', $str_tids)) { + $separator = ','; + $tids = explode(',', $str_tids); + } + else { + return; + } + $translated_tids = array(); + foreach ($tids as $tid) { + if ($translated_tid = i18ntaxonomy_translation_term_tid($tid, $lang)) { + $translated_tids[] = $translated_tid; + } + } + return implode($separator, $translated_tids); +} + +/** + * Implementation of hook_taxonomy(). + * + * $edit parameter may be an array or an object !! + */ +function i18ntaxonomy_taxonomy($op, $type, $edit = NULL) { + global $language; + $edit = (array)$edit; + + switch ("$type/$op") { + case 'term/insert': + case 'term/update': + switch (i18ntaxonomy_vocabulary($edit['vid'])) { + case I18N_TAXONOMY_LOCALIZE: // Update strings for localizable vocabulary. + $tid = $edit['tid']; + i18nstrings_update("taxonomy:term:$tid:name", $edit['name']); + i18nstrings_update("taxonomy:term:$tid:description", $edit['description']); + break; + case I18N_TAXONOMY_LANGUAGE; // Predefined language for all terms + if (empty($edit['language']) && ($voc = taxonomy_vocabulary_load($edit['vid']))) { + _i18ntaxonomy_term_set_lang($edit['tid'], $voc->language); + } + break; + case I18N_TAXONOMY_TRANSLATE: // Multilingual terms, translatable + if (empty($edit['language'])) { + if (!empty($edit['i18ntaxonomy_form'])) { + // Only for the case the term has no language, it may need to be removed from translation set + _i18ntaxonomy_term_set_lang($edit['tid'], NULL); + } elseif($lang = _i18n_get_context_lang()) { + // The term may come from a node tags field, just if this is not a taxonomy form + _i18ntaxonomy_term_set_lang($edit['tid'], $lang); + } else { + // Not from the taxonomy form nor node form, set current language + _i18ntaxonomy_term_set_lang($edit['tid'], $language->language); + } + } + break; + } + break; + + case 'vocabulary/insert': + case 'vocabulary/update': + $vid = $edit['vid']; + // Update vocabulary settings. + if (isset($edit['i18nmode'])) { + i18ntaxonomy_vocabulary($vid, $edit['i18nmode']); + + $edit_lang = isset($edit['language']) ? $edit['language'] : ''; + db_query("UPDATE {vocabulary} SET language='%s' WHERE vid = %d", $edit_lang, $edit['vid']); + if ($edit_lang && $op == 'update') { + db_query("UPDATE {term_data} SET language='%s' WHERE vid = %d", $edit_lang, $edit['vid']); + drupal_set_message(t('Reset language for all terms.')); + } + // Always add vocabulary translation if !$language. + if (!$edit_lang) { + i18nstrings_update("taxonomy:vocabulary:$vid:name", $edit['name']); + } + } + break; + + case 'term/delete': + $tid = $edit['tid']; + i18nstrings_remove_string("taxonomy:term:$tid:name"); + i18nstrings_remove_string("taxonomy:term:$tid:description"); + break; + + case 'vocabulary/delete': + $vid = $edit['vid']; + i18nstrings_remove_string("taxonomy:vocabulary:$vid:name"); + break; + + } +} + +/** + * Implementation of hook_db_rewrite_sql(). + */ +function i18ntaxonomy_db_rewrite_sql($query, $primary_table, $primary_key) { + // No rewrite for administration pages or mode = off. + $mode = i18n_selection_mode(); + if ($mode == 'off' || arg(0) == 'admin') return; + + switch ($primary_table) { + case 't': + case 'v': + // Taxonomy queries. + // When loading specific terms, vocabs, language conditions shouldn't apply. + if (preg_match("/WHERE.* $primary_table\.tid\s*(=\s*\d|IN)/", $query)) return; + // Taxonomy for specific node, or when using the term_node table. + if (preg_match("/WHERE r\.nid = \%d/", $query)) return; + if (preg_match("/{term_node}/", $query)) return; + $result['where'] = i18n_db_rewrite_where($primary_table, 'taxonomy', $mode); + return $result; + } +} + +/** + * Implementation of hook_form_alter(). + * + * This is the place to add language fields to all forms. + * + * @ TO DO The vocabulary form needs some javascript. + */ +function i18ntaxonomy_form_alter(&$form, $form_state, $form_id) { + switch ($form_id) { + case 'taxonomy_overview_vocabularies': + $vocabularies = taxonomy_get_vocabularies(); + $languages = locale_language_list('name'); + foreach ($vocabularies as $vocabulary) { + if ($vocabulary->language) { + $form[$vocabulary->vid]['types']['#value'] .= ' ('. $languages[$vocabulary->language] .')'; + } + } + break; + + case 'taxonomy_overview_terms': + $mode = i18ntaxonomy_vocabulary($form['#vocabulary']['vid']); + if ($mode == I18N_TAXONOMY_TRANSLATE) { + $languages = locale_language_list('name'); + foreach (element_children($form) as $key) { + if (isset($form[$key]['#term']) && ($lang = $form[$key]['#term']['language'])) { + $form[$key]['view']['#value'] .= ' ('. $languages[$lang] .')'; + } + } + } + break; + + case 'taxonomy_form_vocabulary': // Taxonomy vocabulary + if (!empty($form['vid']['#value'])) { + $vocabulary = taxonomy_vocabulary_load($form['vid']['#value']); + $mode = i18ntaxonomy_vocabulary($vocabulary->vid); + } + else { + $vocabulary = NULL; + $mode = I18N_TAXONOMY_NONE; + } + drupal_add_js(drupal_get_path('module', 'i18ntaxonomy') . '/i18ntaxonomy.js'); + drupal_add_js(array('i18ntaxonomy_vocabulary_form' => array('I18N_TAXONOMY_LANGUAGE' => I18N_TAXONOMY_LANGUAGE)), 'setting'); + $form['i18n'] = array( + '#type' => 'fieldset', + '#title' => t('Multilingual options'), + '#collapsible' => TRUE, + '#weight' => 0, + ); + $form['i18n']['i18nmode'] = array( + '#type' => 'radios', + '#title' => t('Translation mode'), + '#options' => _i18ntaxonomy_vocabulary_options(), + '#default_value' => $mode, + '#description' => t('For localizable vocabularies, to have all terms available for translation visit the translation refresh page.', array('@locale-refresh' => url('admin/build/translate/refresh'))), + ); + $form['i18n']['language'] = array( + '#type' => 'select', + '#title' => t('Language'), + '#default_value' => $vocabulary && !empty($vocabulary->language) ? $vocabulary->language : '', + '#options' => array('' => '') + locale_language_list('name'), + '#description' => t('Language for this vocabulary. If set, it will apply to all terms in this vocabulary.'), + '#disabled' => ($vocabulary && $mode != I18N_TAXONOMY_LANGUAGE), + ); + $form['#validate'][] = 'i18ntaxonomy_form_vocabulary_validate'; + break; + + case 'taxonomy_form_term': // Taxonomy term + // Check for confirmation forms + if (isset($form_state['confirm_delete']) || isset($form_state['confirm_parents'])) return; + + $vocabulary = (object)$form['#vocabulary']; + $term = (object)$form['#term']; + + // Mark form so we can know later when saving the term it came from a taxonomy form + $form['i18ntaxonomy_form'] = array('#type' => 'value', '#value' => 1); + + // Add language field or not depending on taxonomy mode. + switch (i18ntaxonomy_vocabulary($vocabulary->vid)) { + case I18N_TAXONOMY_TRANSLATE: + $form['identification']['language'] = array( + '#type' => 'select', + '#title' => t('Language'), + '#default_value' => isset($term) && !empty($term->language) ? $term->language : '', + '#options' => array('' => '') + locale_language_list('name'), + '#description' => t('This term belongs to a multilingual vocabulary. You can set a language for it.'), + ); + break; + + case I18N_TAXONOMY_LANGUAGE: + $form['language'] = array( + '#type' => 'value', + '#value' => $vocabulary->language + ); + $form['identification']['language_info'] = array('#value' => t('All terms in this vocabulary have a fixed language: %language', array('%language' => i18n_language_property($vocabulary->language, 'name')))); + break; + + case I18N_TAXONOMY_LOCALIZE: + $form['language'] = array( + '#type' => 'value', + '#value' => '' + ); + $form['identification']['name']['#description'] .= ' '. t('This name will be localizable. You can translate it using the translate interface pages.', array('@translate-interface' => url('admin/build/translate'))) .''; + $form['identification']['description']['#description'] .= ' '. t('This description will be localizable. You can translate it using the translate interface pages.', array('@translate-interface' => url('admin/build/translate'))) .''; + break; + + case I18N_TAXONOMY_NONE: + default: + $form['language'] = array( + '#type' => 'value', + '#value' => '' + ); + break; + } + break; + case 'search_form': + // Localize category selector in advanced search form. + if (!empty($form['advanced']) && !empty($form['advanced']['category'])) { + i18ntaxonomy_form_all_localize($form['advanced']['category']); + } + break; + default: + if (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] .'_node_form' == $form_id + && ($node = $form['#node']) && isset($form['taxonomy']) && !variable_get('taxonomy_override_selector', FALSE)) { + // Node form. Translate vocabularies. + i18ntaxonomy_node_form($form); + } + } +} + +function i18ntaxonomy_form_vocabulary_validate($form, &$form_state) { + $language = !empty($form_state['values']['language']) ? $form_state['values']['language'] : ''; + $mode = $form_state['values']['i18nmode']; + if ($mode != I18N_TAXONOMY_LANGUAGE && $language) { + form_set_error('language', t('Setting a vocabulary language only makes sense in the "Set language to vocabulary" translation mode. Either change to this mode or do not select a language.')); + } + elseif ($mode == I18N_TAXONOMY_LANGUAGE && !$language ) { + form_set_error('language', t('If selecting "Set language to vocabulary" you need to set a language to this vocabulary. Either change the translation mode or select a language.')); + } +} + +/** + * Localize a taxonomy_form_all() kind of control + * + * The options array is indexed by vocabulary name and then by term id, with tree structure + * We just need to localize vocabulary name and localizable terms. Multilingual vocabularies + * should have been taken care of by query rewriting. + **/ +function i18ntaxonomy_form_all_localize(&$item) { + $options = &$item['#options']; + foreach (taxonomy_get_vocabularies() as $vid => $vocabulary) { + if (!empty($options[$vocabulary->name])) { + // Localize vocabulary name if translated + $vname = i18ntaxonomy_translate_vocabulary_name($vocabulary->name); + if ($vname != $vocabulary->name) { + $options[$vname] = $options[$vocabulary->name]; + unset($options[$vocabulary->name]); + } + if (i18ntaxonomy_vocabulary($vid) == I18N_TAXONOMY_LOCALIZE) { + $tree = taxonomy_get_tree($vid); + if ($tree && (count($tree) > 0)) { + foreach ($tree as $term) { + if (isset($options[$vname][$term->tid])) { + $options[$vname][$term->tid] = str_repeat('-', $term->depth) . i18ntaxonomy_translate_term_name($term->tid, $term->name); + } + } + } + } + } + } +} + +/** + * Handle node form taxonomy. + */ +function i18ntaxonomy_node_form(&$form) { + $node = $form['#node']; + if (!isset($node->taxonomy)) { + $terms = taxonomy_node_get_terms($node); + } + else { + $terms = $node->taxonomy; + } + + // Regenerate the whole field for translatable vocabularies. + foreach (element_children($form['taxonomy']) as $vid) { + if ($vid == 'tags') { + // Special treatment for tags, add some help texts + foreach (element_children($form['taxonomy']['tags']) as $vid) { + $type = i18ntaxonomy_vocabulary($vid); + if ($type == I18N_TAXONOMY_LOCALIZE || $type == I18N_TAXONOMY_TRANSLATE) { + $form['taxonomy']['tags'][$vid]['#title'] = i18ntaxonomy_translate_vocabulary_name($vid, $form['taxonomy']['tags'][$vid]['#title']); + $form['taxonomy']['tags'][$vid]['#description'] = i18nstrings("taxonomy:vocabulary:$vid:help", $form['taxonomy']['tags'][$vid]['#description']); + } + if ($type == I18N_TAXONOMY_LOCALIZE) { + $form['taxonomy']['tags'][$vid]['#description'] .= ' '. t('This is a localizable vocabulary, so only terms in %language are allowed here.', array('%language' => language_default('name'))); + } + } + } + elseif (is_numeric($vid) && i18ntaxonomy_vocabulary($vid) == I18N_TAXONOMY_LOCALIZE) { + // Rebuild this vocabulary's form. + $vocabulary = taxonomy_vocabulary_load($vid); + // Extract terms belonging to the vocabulary in question. + $default_terms = array(); + foreach ($terms as $term) { + if ($term->vid == $vid) { + $default_terms[$term->tid] = $term; + } + } + $form['taxonomy'][$vid] = i18ntaxonomy_vocabulary_form($vocabulary->vid, array_keys($default_terms)); + $form['taxonomy'][$vid]['#weight'] = $vocabulary->weight; + $form['taxonomy'][$vid]['#required'] = $vocabulary->required; + $form['taxonomy'][$vid]['#description'] = i18nstrings("taxonomy:vocabulary:$vid:help", $vocabulary->help); + } + elseif (is_numeric($vid) && i18ntaxonomy_vocabulary($vid) == I18N_TAXONOMY_TRANSLATE) { + // Rebuild this vocabulary's form. + $vocabulary = taxonomy_vocabulary_load($vid); + $form['taxonomy'][$vid]['#title'] = i18ntaxonomy_translate_vocabulary_name($vid, $vocabulary->name); + $form['taxonomy'][$vid]['#description'] = i18nstrings("taxonomy:vocabulary:$vid:help", $vocabulary->help); + } + } +} + +/** + * Generate a form element for selecting terms from a vocabulary. + * Translates all translatable strings. + */ +function i18ntaxonomy_vocabulary_form($vid, $value = 0, $help = NULL) { + $vocabulary = taxonomy_vocabulary_load($vid); + $help = ($help) ? $help : i18nstrings("taxonomy:vocabulary:$vid:help", $vocabulary->help); + + if (!$vocabulary->multiple) { + $blank = ($vocabulary->required) ? t('- Please choose -') : t('- None selected -'); + } + else { + $blank = ($vocabulary->required) ? 0 : t('- None -'); + } + $tree = i18ntaxonomy_localize_terms(taxonomy_get_tree($vid)); + return _i18ntaxonomy_term_select(i18ntaxonomy_translate_vocabulary_name($vocabulary), $value, $tree, $help, intval($vocabulary->multiple), $blank); +} + +/** + * Produces tree for taxonomy vocabularies. + * + * The difference with _taxonomy_term_select() is that this function is passed the term tree + * that may be already localized or filtered by language + */ +function _i18ntaxonomy_term_select($title, $value, $tree, $description = '', $multiple = FALSE, $blank = '--', $exclude = array()) { + + $options = array(); + + if ($blank) { + $options[''] = $blank; + } + if ($tree) { + foreach ($tree as $term) { + if (!in_array($term->tid, $exclude)) { + $choice = new stdClass(); + $choice->option = array($term->tid => str_repeat('--', $term->depth) . $term->name); + $options[] = $choice; + } + } + } + + return array( + '#type' => 'select', + '#title' => $title, + '#default_value' => $value, + '#options' => $options, + '#description' => $description, + '#multiple' => $multiple, + '#size' => $multiple ? min(9, count($options)) : 0, + '#weight' => -15, + '#theme' => 'taxonomy_term_select', + ); +} + +/** + * Helper function for + */ +/** + * Set language for a term. If no language set trid to 0 too. + */ +function _i18ntaxonomy_term_set_lang($tid, $langcode) { + if ($langcode) { + db_query("UPDATE {term_data} SET language='%s' WHERE tid = %d", $langcode, $tid); + } else { + db_query("UPDATE {term_data} SET language = '', trid = 0 WHERE tid = %d", $tid); + } +} + +/** + * Multilingual Taxonomy. + */ + +/** + * Get term translations for multilingual terms. This works for multilingual vocabularies. + * + * @param $params + * Array of query conditions. I.e. array('tid' => xxx) + * @param $getall + * Whether to get the original term too in the set or not. + * + * @return + * An array of the from lang => Term. + */ +function i18ntaxonomy_term_get_translations($params, $getall = TRUE) { + foreach ($params as $field => $value) { + $conds[] = "i.$field = '%s'"; + $values[] = $value; + } + if (!$getall) { // If not all, a parameter must be tid. + $conds[] = "t.tid != %d"; + $values[] = $params['tid']; + } + $conds[] = "t.trid != 0"; + $sql = 'SELECT t.* FROM {term_data} t INNER JOIN {term_data} i ON t.trid = i.trid WHERE '. implode(' AND ', $conds);; + $result = db_query($sql, $values); + $items = array(); + while ($data = db_fetch_object($result)) { + $items[$data->language] = $data; + } + return $items; +} + +/** + * Like nat_get_terms() but without caching. + */ +function i18ntaxonomy_nat_get_terms($nid) { + $return = array(); + + $result = db_query("SELECT td.* FROM {nat} n INNER JOIN {term_data} td USING (tid) WHERE n.nid = %d", $nid); + while ($term = db_fetch_object($result)) { + $return[$term->tid] = $term; + } + + return $return; +} + +/** + * Implementation of hook_nodeapi(). + * + * Prepare node for translation. + */ +function i18ntaxonomy_nodeapi(&$node, $op, $teaser, $page) { + switch ($op) { + case 'view': + // This runs after taxonomy:nodeapi, so we just localize terms here. + if (!empty($node->taxonomy)) { + $node->taxonomy = i18ntaxonomy_localize_terms($node->taxonomy); + } + if ($node->type == 'forum' && ($vid = variable_get('forum_nav_vocabulary', '')) && i18ntaxonomy_vocabulary($vid)) { + if ($page && taxonomy_node_get_terms_by_vocabulary($node, $vid) && $tree = taxonomy_get_tree($vid)) { + // Breadcrumb navigation + $vocabulary = taxonomy_vocabulary_load($vid); + $breadcrumb[] = l(t('Home'), NULL); + $breadcrumb[] = l(i18nstrings("taxonomy:vocabulary:$vid:name", $vocabulary->name), 'forum'); + // Translate node taxonomy terms. Sometimes there are no terms, like for search results... + if (!empty($node->taxonomy)) { + // Get the forum terms from the (cached) tree + foreach ($tree as $term) { + $forum_terms[] = $term->tid; + } + foreach ($node->taxonomy as $term_id => $term) { + if (in_array($term_id, $forum_terms)) { + $node->tid = $term_id; + } + } + + if ($parents = taxonomy_get_parents_all($node->tid)) { + $parents = array_reverse($parents); + foreach ($parents as $p) { + $breadcrumb[] = l(i18nstrings("taxonomy:term:$term->tid:name", $p->name), 'forum/'. $p->tid); + } + } + } + drupal_set_breadcrumb($breadcrumb); + } + } + break; + + case 'prepare translation': + $source = $node->translation_source; + // Taxonomy translation. + if (is_array($source->taxonomy)) { + // Set translated taxonomy terms. + $node->taxonomy = i18ntaxonomy_translate_terms($source->taxonomy, $node->language); + } + break; + } +} + +/** + * Find all terms associated with the given node, ordered by vocabulary and term weight. + * + * Same as taxonomy_node_get_terms() but without static caching. + */ +function i18ntaxonomy_node_get_terms($node, $key = 'tid') { + $result = db_query(db_rewrite_sql('SELECT t.* FROM {term_node} r INNER JOIN {term_data} t ON r.tid = t.tid INNER JOIN {vocabulary} v ON t.vid = v.vid WHERE r.vid = %d ORDER BY v.weight, t.weight, t.name', 't', 'tid'), $node->vid); + $terms = array(); + while ($term = db_fetch_object($result)) { + $terms[$term->$key] = $term; + } + return $terms; +} + +/** + * Translate an array of taxonomy terms. + * + * Translates all terms with language, just passing over terms without it. + * Filter out terms with a different language + * + * @param $taxonomy + * Array of term objects or tids or multiple arrays or terms indexed by vid + * @param $langcode + * Language code of target language + * @param $fullterms + * Whether to return full $term objects, returns tids otherwise + * @return + * Array with translated terms: tid -> $term + * Array with vid and term array + */ +function i18ntaxonomy_translate_terms($taxonomy, $langcode, $fullterms = TRUE) { + $translation = array(); + if (is_array($taxonomy) && $taxonomy) { + foreach ($taxonomy as $index => $tdata) { + if (is_array($tdata)) { + // Case 1: Index is vid, $tdata is an array of terms + $mode = i18ntaxonomy_vocabulary($index); + // We translate just some vocabularies: translatable, fixed language + // Fixed language ones may have terms translated, though the UI doesn't support it + if ($mode == I18N_TAXONOMY_LANGUAGE || $mode == I18N_TAXONOMY_TRANSLATE) { + $translation[$index] = i18ntaxonomy_translate_terms($tdata, $langcode, $filter, $fullterms); + } + elseif ($fullterms) { + $translation[$index] = array_map('_i18ntaxonomy_filter_terms', $tdata); + } + else { + $translation[$index] = array_map('_i18ntaxonomy_filter_tids', $tdata); + } + continue; + } + elseif (is_object($tdata)) { + // Case 2: This is a term object + $term = $tdata; + } + elseif (is_numeric($tdata) && ($tid = (int)$tdata)) { + // Case 3: This is a term tid, load the full term + $term = taxonomy_get_term($tid); + } + // Translate the term if we got it + if (empty($term)) { + // Couldn't identify term, pass through whatever it is + $translation[$index] = $tdata; + } + elseif ($term->language && $term->language != $langcode) { + $translated_terms = i18ntaxonomy_term_get_translations(array('tid' => $term->tid)); + if ($translated_terms && !empty($translated_terms[$langcode])) { + $newterm = $translated_terms[$langcode]; + $translation[$newterm->tid] = $fullterms ? $newterm : $newterm->tid; + } + } + else { + // Term has no language. Should be ok. + $translation[$index] = $fullterms ? $term : $term->tid; + } + } + } + return $translation; +} + +/** + * Localize taxonomy terms for localizable vocabularies. + * + * @param $terms + * Array of term objects. + * @param $fields + * Object properties to localize. + * @return + * Array of terms with the right ones localized. + */ +function i18ntaxonomy_localize_terms($terms, $fields = array('name', 'description')) { + $localize = i18ntaxonomy_vocabulary(NULL, I18N_TAXONOMY_LOCALIZE); + foreach ($terms as $index => $term) { + if (in_array($term->vid, $localize)) { + // Clone objects just in case one of them is saved later + $terms[$index] = clone $term; + foreach ($fields as $property) { + $terms[$index]->$property = i18nstrings("taxonomy:term:$term->tid:$property", $term->$property); + } + } + } + return $terms; +} + +/** + * Taxonomy vocabulary settings. + * + * - If $vid and not $value, returns mode for vid. + * - If $vid and $value, sets mode for vid. + * - If !$vid and !$value returns all settings. + * - If !$vid and $value returns all vids for this mode. + * + * @param $vid + * Vocabulary id. + * @param $value + * Vocabulary mode. + * + */ +function i18ntaxonomy_vocabulary($vid = NULL, $mode = NULL) { + $options = variable_get('i18ntaxonomy_vocabulary', array()); + + if ($vid && !is_null($mode)) { + $options[$vid] = $mode; + variable_set('i18ntaxonomy_vocabulary', $options); + } + elseif ($vid) { + return array_key_exists($vid, $options) ? $options[$vid] : I18N_TAXONOMY_NONE; + } + elseif (!is_null($mode)) { + return array_keys($options, $mode); + } + else { + return $options; + } +} + +/** + * Returns a list for terms for vocabulary, language. + * + * @param $vid + * Vocabulary id + * @param $lang + * Language code + * @param $status + * 'all' (default), 'translated', 'untranslated' + */ +function i18ntaxonomy_vocabulary_get_terms($vid, $lang, $status = 'all') { + switch ($status) { + case 'translated': + $result = db_query("SELECT * FROM {term_data} WHERE vid = %d AND language = '%s' AND trid > 0", $vid, $lang); + break; + + case 'untranslated': + $result = db_query("SELECT * FROM {term_data} WHERE vid = %d AND language = '%s' AND trid = 0", $vid, $lang); + break; + + default: + $result = db_query("SELECT * FROM {term_data} WHERE vid = %d AND language = '%s'", $vid, $lang); + break; + } + $list = array(); + while ($term = db_fetch_object($result)) { + $list[$term->tid] = $term->name; + } + return $list; +} + +/** + * Get taxonomy tree for a given language + * + * @param $vid + * Vocabulary id + * @param $lang + * Language code + * @param $parent + * Parent term id for the tree + */ +function i18ntaxonomy_get_tree($vid, $lang, $parent = 0, $depth = -1, $max_depth = NULL) { + static $children, $parents, $terms; + + $depth++; + + // We cache trees, so it's not CPU-intensive to call get_tree() on a term + // and its children, too. + if (!isset($children[$vid][$lang])) { + $children[$vid][$lang] = array(); + + $result = db_query(db_rewrite_sql("SELECT t.tid, t.*, parent FROM {term_data} t INNER JOIN {term_hierarchy} h ON t.tid = h.tid WHERE t.vid = %d AND t.language = '%s' ORDER BY weight, name", 't', 'tid'), $vid, $lang); + while ($term = db_fetch_object($result)) { + $children[$vid][$lang][$term->parent][] = $term->tid; + $parents[$vid][$lang][$term->tid][] = $term->parent; + $terms[$vid][$term->tid] = $term; + } + } + + $max_depth = (is_null($max_depth)) ? count($children[$vid][$lang]) : $max_depth; + $tree = array(); + if (!empty($children[$vid][$lang][$parent])) { + foreach ($children[$vid][$lang][$parent] as $child) { + if ($max_depth > $depth) { + $term = drupal_clone($terms[$vid][$child]); + $term->depth = $depth; + // The "parent" attribute is not useful, as it would show one parent only. + unset($term->parent); + $term->parents = $parents[$vid][$lang][$child]; + $tree[] = $term; + + if (!empty($children[$vid][$lang][$child])) { + $tree = array_merge($tree, i18ntaxonomy_get_tree($vid, $lang, $child, $depth, $max_depth)); + } + } + } + } + + return $tree; +} + +/** + * Implementation of hook_token_values(). + */ +function i18ntaxonomy_token_values($type, $object = NULL, $options = array()) { + $values = array(); + switch ($type) { + case 'taxonomy': + $term = $object; + $values['i18n-term-raw'] = i18nstrings("taxonomy:term:$term->tid:name", $term->name); + $values['i18n-term'] = check_plain(i18nstrings("taxonomy:term:$term->tid:name", $term->name)); + break; + + case 'node': + $node = $object; + // This code is copied from the token module which i adapting + // pathauto's handling code; it's intended for compatability with it. + if (!empty($node->taxonomy) && is_array($node->taxonomy)) { + foreach ($node->taxonomy as $term) { + $original_term = $term; + if ((object)$term) { + // With freetagging it's somewhat hard to get the tid, vid, name values + // Rather than duplicating taxonomy.module code here you should make sure your calling module + // has a weight of at least 1 which will run after taxonomy has saved the data which allows us to + // pull it out of the db here. + if (!isset($term->name) || !isset($term->tid)) { + $vid = db_result(db_query_range("SELECT t.vid FROM {term_node} r INNER JOIN {term_data} t ON r.tid = t.tid INNER JOIN {vocabulary} v ON t.vid = v.vid WHERE r.nid = %d ORDER BY v.weight, t.weight, t.name", $object->nid, 0, 1)); + if (!$vid) { + continue; + } + $term = db_fetch_object(db_query_range("SELECT t.tid, t.name FROM {term_data} t INNER JOIN {term_node} r ON r.tid = t.tid WHERE t.vid = %d AND r.nid = %d ORDER BY weight", $vid, $object->nid, 0, 1)); + $term->vid = $vid; + } + + // Ok, if we still don't have a term name maybe this is a pre-taxonomy submit node + // So if it's a number we can get data from it + if (!isset($term->name) && is_array($original_term)) { + $tid = array_shift($original_term); + if (is_numeric($tid)) { + $term = taxonomy_get_term($tid); + } + } + $vid = $term->vid; + // If term names are localizable, we translate them to the node's + // content language, not to the interface language' in which the + // current user is viewing the site. (Creation of node tokens should + // not depend on 'unpredictable' conditions like these.) + // If node is language neutral, language is set to NULL. + if (i18ntaxonomy_vocabulary($vid) == I18N_TAXONOMY_LOCALIZE && $node->language) { + $values['i18n-term-raw'] = i18nstrings("taxonomy:term:$term->tid:name", $term->name, $node->language); + $values['i18n-term'] = check_plain(i18nstrings("taxonomy:term:$term->tid:name", $term->name, $node->language)); + } + else { + $values['i18n-term-raw'] = $term->name; + $values['i18n-term'] = check_plain($term->name); + } + break; + } + } + } + // It's possible to leave that block and still not have good data. + // So, we test for these and if not set, set them. + if (!isset($values['i18n-term'])) { + $values['i18n-term-raw'] = ''; + $values['i18n-term'] = ''; + } + break; + } + return $values; +} + +/** + * Implementation of hook_token_list(). + */ +function i18ntaxonomy_token_list($type = 'all') { + if ($type == 'node' || $type == 'all' || $type == 'taxonomy') { + $tokens['i18ntaxonomy']['i18n-term-raw'] = t("Unescaped term name translated using i18n"); + $tokens['i18ntaxonomy']['i18n-term'] = t("Escaped term name translated using i18n"); + return $tokens; + } +} + +/** + * Translate forums list. + */ +function i18ntaxonomy_preprocess_forum_list(&$variables) { + $vid = variable_get('forum_nav_vocabulary', ''); + if (i18ntaxonomy_vocabulary($vid)) { + foreach ($variables['forums'] as $id => $forum) { + $variables['forums'][$id]->description = i18nstrings('taxonomy:term:'. $forum->tid .':description', $forum->description); + $variables['forums'][$id]->name = i18nstrings('taxonomy:term:'. $forum->tid .':name', $forum->name); + } + } +} + +/** + * Translate forum page. + */ +function i18ntaxonomy_preprocess_forums(&$variables) { + $vid = variable_get('forum_nav_vocabulary', ''); + if (i18ntaxonomy_vocabulary($vid)) { + if (isset($variables['links']['forum'])) { + $variables['links']['forum']['title'] = i18nstrings('nodetype:type:forum:post_button', 'Post new Forum topic'); + } + // This one is from advanced forum, http://drupal.org/project/advanced_forum + if ($variables['forum_description']) { + $variables['forum_description'] = i18nstrings('taxonomy:term:'. $variables['tid'] .':description', $variables['forum_description']); + } + + $vocabulary = taxonomy_vocabulary_load($vid); + // Translate the page title. + $title = !empty($vocabulary->name) ? i18ntaxonomy_translate_vocabulary_name($vocabulary) : ''; + drupal_set_title($title); + + // Translate the breadcrumb. + $breadcrumb = array(); + $breadcrumb[] = l(t('Home'), NULL); + $breadcrumb[] = l($title, 'forum'); + drupal_set_breadcrumb($breadcrumb); + } +} + +/** + * Recursive array filtering, convert all terms to 'tid'. + */ +function _i18ntaxonomy_filter_tids($tid) { + if (is_array($tid)) { + return array_map('_i18n_taxonomy_filter_tids', $tid); + } + else { + return is_object($tid) ? $tid->tid : (int)$tid; + } +} + +/** + * Recursive array filtering, convert all terms to 'term object' + */ +function _i18ntaxonomy_filter_terms($term) { + if (is_array($term)) { + return array_map('_i18n_taxonomy_filter_terms', $term); + } + else { + return is_object($term) ? $term : taxonomy_get_term($term); + } +} diff --git a/sites/all/modules/i18n/i18ntaxonomy/i18ntaxonomy.pages.inc b/sites/all/modules/i18n/i18ntaxonomy/i18ntaxonomy.pages.inc new file mode 100644 index 0000000..43cc0e9 --- /dev/null +++ b/sites/all/modules/i18n/i18ntaxonomy/i18ntaxonomy.pages.inc @@ -0,0 +1,150 @@ +tid; + $names[] = i18ntaxonomy_translate_term_name($term); + } + + if ($names) { + $title = check_plain(implode(', ', $names)); + drupal_set_title($title); + + switch ($op) { + case 'page': + // Build breadcrumb based on first hierarchy of first term: + $current->tid = $tids[0]; + $breadcrumb = array(); + while ($parents = taxonomy_get_parents($current->tid)) { + $current = array_shift($parents); + $breadcrumb[] = l($current->name, 'taxonomy/term/'. $current->tid); + } + $breadcrumb[] = l(t('Home'), NULL); + $breadcrumb = array_reverse($breadcrumb); + drupal_set_breadcrumb($breadcrumb); + + $output = theme('i18ntaxonomy_term_page', $tids, taxonomy_select_nodes($tids, $terms['operator'], $depth, TRUE)); + drupal_add_feed(url('taxonomy/term/'. $str_tids .'/'. $depth .'/feed'), 'RSS - '. $title); + return $output; + break; + + case 'feed': + $channel['link'] = url('taxonomy/term/'. $str_tids .'/'. $depth, array('absolute' => TRUE)); + $channel['title'] = variable_get('site_name', 'Drupal') .' - '. $title; + + // Only display the description if we have a single term, to avoid clutter and confusion. + if (count($tids) == 1) { + $terms = array(taxonomy_get_term($tids[0])); + $terms = i18ntaxonomy_localize_terms($terms, array('description')); + $terms['operator'] = 'or'; + // HTML will be removed from feed description, so no need to filter here. + $channel['description'] = $terms[0]->description; + } + + $result = taxonomy_select_nodes($tids, $terms['operator'], $depth, FALSE); + $items = array(); + while ($row = db_fetch_object($result)) { + $items[] = $row->nid; + } + + node_feed($items, $channel); + break; + + default: + drupal_not_found(); + } + } + else { + drupal_not_found(); + } + } +} + +/** + * 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_i18ntaxonomy_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 = taxonomy_get_term($tids[0]); + if (i18ntaxonomy_vocabulary($term->vid) == I18N_TAXONOMY_LOCALIZE) { + $description = i18nstrings("taxonomy:term:$term->tid:description", $term->description); + } + else { + $description = $term->description; + } + + // Check that a description is set. + if (!empty($description)) { + $output .= '