FINAL suepr merge step : added all modules to this super repos

This commit is contained in:
Bachir Soussi Chiadmi
2015-04-19 16:46:59 +02:00
7585 changed files with 1723356 additions and 18 deletions

View File

@@ -0,0 +1,271 @@
Entity Translation 7.x-1.x, xxxx-xx-xx
--------------------------------------
Entity Translation 7.x-1.0-beta3, 2013-07-23
--------------------------------------------
#2037789 by Jelle_S: Fixed Default to user_access() access callback like drupal
menu does.
#1982140 by bforchhammer | carligraph: Fixed Menu item is deleted when adding a
third translation.
#2000926 by GiorgosK, bforchhammer: Fixed entity_translation_i18n_menu should
check if entity is node before deleting menu links.
#1991452 by twistor: Fixed Strict warnings on testing page.
#1992588 by bforchhammer: Fixed PHP notices on node page (menu-related).
#1979406 by milesw: Field values for node translations not filled correctly when
making a field translatable.
#1969366 by plach | EdNet: Fixed unsupported operand types in
entity_translation().module line 951 - Unable to clear cache, run cron,
etc from drush or aegir.
#1970246 by jweowu: Fixed entity_translation_upgrade() doesn't flush the entity
load cache.
#1959614 by plach | rutcreate: Fixed Image is deleted by saving node 5 times.
#1941080 by plopesc, bforchhammer: Fixed Menu translation on node form is
broken.
#1937242 by eliosh: Use custom theme function for node translation overview.
#1916738 by alberto56, plach: Added Make it more obvious to those new to the
concept of field translation that all instances of the field are translatable.
#1942732 by OnkelTem: Fixed WSOD when LEFT JOIN'ing entities' translations.
#1942712 by das-peter: Fixed Non wrapped entity passed to Rules.
#1907456 by leon.nk: Added Allow bulk field updates to copy data for all
translations.
#1947764 by plach: Improve bundle translatability checks.
#1370900 by mojzis, plach | mgladding: Fixed Fatal Error: Cannot access empty
property in translation.handler.inc.
Issue #1924088 by plach | drzraf: Fixed Wrong form 'language' value in case of
content-translation enabled bundles.
#1933742 by das-peter: Fixed Delete translation doesn't flush the entity cache.
#1903024 by fabsor | dimitrileonidas: Fixed SQL error in Views when adding an
entity translation relationship that is not using the base entity of the view.
#1890346 by plach: Fixed Entity language change not handled on non-translatable
entities with translatable fields.
#1883584 by grndlvl: Added Allow for link 'destination' on overview page.
#1888324 by plach: Fixed Wrong form language set when only one language is
enabled and the entity has LANGUAGE_NONE language.
#1888192 by plach: Fixed Field widgets hidden when no #access key is defined.
#1885370 by grndlvl: Fixed 'View' title blank on translation overview when using
optional title replacement field.
#1881872 by plach | Demoshane: Added an explict dependecy on Drupal core 7.15.
#1876750 by David_Rothstein: Fixed Fatal error when creating an entity as an
anonymous user.
#1760270 by plach | Berdir: Fixed Comment translation broken?.
#1870236 by plach: Fixed Field translatability migration fails when translation
for the entity type is disabled.
#1865176 by bforchhammer: Implement hook_entity_translation_delete() on behalf
of path module.
#1862882 by Cottser: Fixed Typo: accross = across.
#1851118 by peximo | impleri: Fixed Incorrect ET Handler set in node/add form
when inserting new taxonomy term.
#1842540 by peximo, bforchhammer | plach: Added Pathauto core entity types
integration.
Entity Translation 7.x-1.0-beta2, 2012-11-21
--------------------------------------------
#1846104 by David_Rothstein, plach: Fixed Entity Translation shouldn't assume
$form['#entity_type()'] is a string.
#1845896 by plach, peximo: Fixed Validate translation authoring info on submit.
#1835578 by farse, plach: Fixed Entity Translation metadata should be stored
only if the entity bundle is translatable.
#1437976 by plach | Wolfgang Reszel: Fixed Invalid node translation menu
callback set when entity translation for nodes is disabled.
#1844844 by plach, bforchhammer: Fixed Content type settings always showed.
#1833496 by bforchhammer, plach: Fixed Upgrade to Beta1 results in 'Cannot
initialize entity translation path variables'.
#1397560 by timofey, fricca, bforchhammer: Fixed Delete translation button not
working if destination parameter is set.
#1829740 by plach: Comment filtering should be available if node translation is
enabled (follow-up).
#1829980 by plach | Stan Shevchuk: Fixed Upgrade to Beta1 results in 'Integrity
constraint violation: 1048 Column 'module' cannot be null'.
Entity Translation 7.x-1.0-beta1, 2012-11-02
--------------------------------------------
#1828794 by plach: Make translation workflow permissions optional.
#1829666 by bforchhammer: Fixed Fatal error caused by menu translation when node
translation is disabled.
#1829740 by plach: Fixed Node and comments ET settings are available also when
translation is disabled.
#1818440 by bforchhammer, plach, miro_dietiker: Fixed Fatal error with unclean
edit path.
#1672710 by plach, valencianok, miro_dietiker | haclong99: Fixed published
source node appears as not published in the translate tab.
#1819844 by plach | bforchhammer, renat: Fixed Incompatibility with fields with
unlimited number of values introduced in ET -dev.
#1818620 by bforchhammer, plach | renat: Fixed fatal error when creating a node
which has a field_collection field.
#1820742 by mkalkbrenner, plach: Fixed parameter $langcode of core function
node_view() ignored.
#1820910 by plach | astutonet: Fixed Notices when trying to filter comments by
language.
#1770748 by bforchhammer, plach: Option to display shared fields only when
editing the original values.
#1811672 by joelrosen: Fixed Entity Translation stuff showing on content types
with multilingual support disabled.
#1810322 by Dave Reid: Fixed Fatal error when using the translate link field
handler in Views.
#1807698 by peximo: Added Hide node translation links.
#1798456 by plach, bforchhammer, Berdir: Hide shared form elements when the user
does not have the related permission (follow up).
#1803362 by Dave Reid: Fixed PHP notice: Undefined index: in
EntityTranslationDefaultHandler->localTasksAlter().
#1800822 by plach, peximo | facine: Fixed Call to a member function
getLanguageKey() on a non-object.
#1799770 by bforchhammer: Updated id and bundle when setting a wrapped entity.
#1798456 by plach: Hide shared form elements when the user has not the related
permission.
#1679736 by plach | renat: Provided an option to lock the original language.
#1798460 by plach: Made the translate operation contextual.
#1280546 by plach: Introduced a language selection widget for every entity.
#1676716 by plach | quiptime: Do not list entity types that do not integrate
with ET in the admin page.
#1792620 by bforchhammer: Cleanup unused variables.
#1418076 by bforchhammer, plach: Added support for multiple path schemes.
#1792606 by plach: Fixed Notices when deleting the translation of an entity
having a file field.
#1770202 by plach: Fixed Language tabs not appearing when creating a new
translation (follow-up).
#1678614 by bforchhammer, plach | Berdir: Make access control for the entity
form in the original language more flexible.
#1701212 by jonhattan: Fixed fatal error for entities with ET disabled (UUID
follow-up).
#1701460 by bforchhammer: Fixed title of original language dropdown.
#1155134 by bforchhammer, plach: Integrated pathauto bulk generation.
#1694478 by bforchhammer: Delete menu items when translation is deleted.
#1770250 by bforchhammer, plach: Fixed Translatable fields disappear when
editing default language (entities other than node).
#1377542 by bforchhammer | Daemon_Byte: Fixed Do not always use the admin theme.
#1772874 by bforchhammer | webdrips: Fixed Wrong query alteration for comment
language filtering.
#1778662 by jherencia: Fixed Add compability with field widgets that uses
limit_validation_errors().
#1770202 by bforchhammer: Fixed Local tasks (language tabs) visible on node view
page.
#1701212 by guillaumev: Added Export entity translations using UUID.
#1728648 by plach, Dave Reid: Fixed Ensure that empty translation data is
handled properly.
#1728674 by Dave Reid, plach: Fixed PHP notice when formLanguage is FALSE in
EntityTranslationDefaultHandler.
#1114410 by plach: Replace hook_translation_info() with hook_entity_info().
#1694480 by plach | bforchhammer: Implement hook_module_implements() instead of
relying on the module weight.
#1434574 by das-peter, plach | cjoy: Added events for rules.
#1224590 by plach: Made entity translation CRUD hooks fire only after storing
the translation data (follow-up).
#1643578 by plach: Added Support menu translation upgrade.
#1713196 by plach: Fixed Entity type labels may not be defined in entity info.
#1444866 by plach: Removed bogus hook menu link alteration (follow-up).
#1699060 by plach: Fixed Bogus form elements on the taxonomy delete confirm
form.
#1699052 by plach: Fixed Global entity form submit handlers are not invoked on
delete except for node.
#1661348 by plach | liquidcms: Fixed minimal i18n Taxonomy integration.
#1444866 by plach: Fixed the invocation order on presave and menu alteration
(follow-up).
#1691026 by plach | FrancescoQ: Fixed Fatal error when enabling ET with just one
language installed.
#1698622 by plach: Fixed Field data is not properly massaged before being stored
during an upgrade.
#1224590 by bforchhammer: Added entity translation CRUD hooks.
#1694472 by bforchhammer: Change module package names to 'Multilingual - Entity
Translation'.
#1444866 by bforchhammer, plach, peximo: Added UI for translation of menu items
for entity-translated nodes.
#1133038 by plach: Added support for field column synchronization.
#1528624 by plach | phm: Fixed error on user translations overview.
#1667582 by peximo: Fixed PDOException (ambiguous tnid field) on upgrade.
#1658424 by torpy: Fixed Non-multilingual taxonomy terms attached to
multilingual nodes do not display.
#1643394 by plach: Allow repeating the upgrade process.
#1643414 by plach: Added Allow to select which content types to upgrade.
#1643390 by plach: Implement hook_entity_translation_upgrade_translation() on
behalf of core modules.
#1155134 by das-peter, plach, GiorgosK, zambrey: Added Integrate pathauto.
#1282018 by plach, Gábor Hojtsy, webchick, bforchhammer: Drop the translation
form in favor of a language-aware edit form.
Entity Translation 7.x-1.0-alpha2, 2012-06-16
---------------------------------------------
#1587108 by plach | divined: Error: The entities of type Taxonomy term do not
define a valid base path: it will not be possible to translate them.
#1524210 by fabsor, plach: Expose entity translation table to views.
#1095390 by plach | klonos: Capitalize first letters in project's name.
#1599568 by torpy: Fixed No content is displayed if fallback is disabled and
entity is set to 'Language Neutral'.
#1341314 by plach, donquixote | derhasi: Fixed Nested call of entity_get_info()
due to menu_get_router() in entity_translation_entity_info_alter().
#1519912 by plach: Fixed Empty stored values may mess up the translatable switch
update.
#1519906 by plach: Migrate aliases when upgrading nodes.
#1519898 by plach: Fixed The upgrade process may freeze.
#1519894 by plach: Fixed Entity Translation upgrade migration incomplete.
#1432206 by bforchhammer: Fixed Loss of field content during translatability
switch operation.
#1279372 by getgood, loganfsmyth, evolvingweb, plach, Kristen Pol: Enable bulk
field language updates when switching field translatability.
#1380380 by bojanz: Fixed Prevent notice when translation has been removed from
the form.
#1367832 by floretan: Fixed Check #parents and #field_parents() for source
language in new translation form.
#1174242 by djac, good_man, plach | Jerome F: Properly override the
node/%node/translate menu router path.
#1293638 by das-peter: Fixed Language fallback on Taxonomy Terms (or any entity
type without locale support).
#1283200 by plach: Enable content language negotiation the right way.
#1280660 by mvc, plach: Fixed Gracefully handle module being disabled and
re-enabled.
#1280544 by mvc, emarchak: Added Clarify difference between multilingual support
options.
#1280506 by tarmstrong | plach: Fixed Simpletests broken after upgrading to
D7.7.
#1262512 by claudiu.cristea: Fixed Clear the entity cache after translate.
#1230858 by tarmstrong, plach | valderama: Fixed Compatibility with
node_clone(); entity_id() of old node is used, resulting in integrity
constraint violation.
Entity Translation 7.x-1.0-alpha1, 2011-09-07
---------------------------------------------
#1045196 by plach: Fixed Image field translation broken.
#1003876 by plach: (follow-up) Fixed labels not replaced by Title.
#944874 by das-peter, plach: Added basic tests for the translation creation and
editing workflow.
#1031370 by joostvdl | rfay: Fixed Translation publishing status is hidden under
collapsed 'publishing options' fieldset (and is hidden by default).
#1003876 by plach, good_man: (follow-up) Fixed original label translation.
#1003876 by good_man, plach: Introduced support for translated entity labels.
#936646 by good_man, fietserwin, plach, klonos: Fixed Undefined index access
callback/arguments in entity_translation_menu().
#1109198 by plach, Countzero: Fixed Node translation overview page broken if
i18n is enabled.
#1098106 by pcambra, jelenex: Fixed Translated fields aren't validated (or
processed with presave and submit field_attach_() hooks).
#1111686 by plach: Fixed API documentation wrong.
#1095390 by plach, klonos, sun: Changed Do not capitalize first letters in
project's name.
#1032846 by plach, sun, das-peter: Removed read-ony non translatable fields from
the translation form.
#1082112 by fietserwin | mattwad: Fixed No argument passed to
_translation_tab_access()
#1081444 by jelenex: Fixed Unable to create non-published node.
#1083704 by jelenex: Fixed Unable to edit node when user has only the 'translate
node entities' permission.
#1096008 by fietserwin, sun: Fixed URL alias not updated.
#1060334 by plach: Completed the transition from Translation 2.
#902760 by sun: Changed module name from translation to entity_translation.
#1032602 by rfay: Fixed bogus 'base path' docs in hook_translation_info().
#1032816 by das-peter: Removed superfluous method getHumanReadableId().
#1032728 by das-peter, rfay, sun: Fixed entity info cache not cleared after
changing module settings.
#1021434 by plach | sun, OnkelTem: Fixed `translation` table is not created on
upgrading from core translation.
#1003320 by good_man | Berdir, pcambra, plach, sun: Fixed The etid insane field.
#1009546 by good_man: Fixed form buttons not wrapped in #type actions.
#990120 by das-peter: Changed view mode from 'full' to 'default' in edit form.
#929444 by das-peter, plach, sun: Fixed coding standards.
#920826 by das-peter, sun: Fixed various issues after initial merge.
#673300 by plach, sun: Added initial Translatable Fields UI code.
by sun: Added initial baseline of module files.

View File

@@ -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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@@ -0,0 +1,38 @@
Entity translation is built and maintained by the Drupal project community.
Everyone is encouraged to submit issues and changes (patches) to improve it, and
to contribute in other ways -- see http://drupal.org/contribute to find out how.
Project maintainers
-------------------
The Entity translation maintainers oversee the development of the project as a
whole. The project maintainers for Entity translation are:
- Francesco Placella 'plach' <http://drupal.org/user/183211>
- Daniel F. Kudwien 'sun' <http://drupal.org/user/54136>
Component maintainers
---------------------
The Entity translation component maintainers oversee the development of its
subsystems. This role maps pretty closely to the one of core maintainer for the
Drupal core project (see http://drupal.org/contribute/core-maintainers for more
information on their responsibilities), with the exception that here component
maintainers are granted commit access for their specific subsystems. See
http://drupal.org/node/363367 to find out how to become a component maintainer.
Current component maintainers for Entity translation:
Base system
- Francesco Placella 'plach' <http://drupal.org/user/183211>
- Benedikt Forchhammer 'bforchhammer' <http://drupal.org/user/216396>
Menu integration
- Benedikt Forchhammer 'bforchhammer' <http://drupal.org/user/216396>
Views integration
- Fabian Sörqvist 'fabsor' <http://drupal.org/user/255704>
Node translation upgrade
- Francesco Placella 'plach' <http://drupal.org/user/183211>

View File

@@ -0,0 +1,37 @@
-- SUMMARY --
Allows entities to be translated into different languages.
For a full description of the module, visit the project page:
http://drupal.org/project/entity_translation
To submit bug reports and feature suggestions, or to track changes:
http://drupal.org/project/issues/entity_translation
-- REQUIREMENTS --
None.
-- INSTALLATION --
* Install as usual, see http://drupal.org/node/70151 for further information.
-- CONFIGURATION --
* @todo
-- USAGE --
* @todo
-- CONTACT --
Current maintainers:
* Francesco Placella (plach) - http://drupal.org/user/183211
* Daniel F. Kudwien (sun) - http://drupal.org/user/54136

View File

@@ -0,0 +1,4 @@
.entity-translation-language-tabs {
clear: both;
padding-top: 0.75em;
}

View File

@@ -0,0 +1,669 @@
<?php
/**
* @file
* The entity translation user interface.
*/
/**
* Builder function for the entity translation settings form.
*/
function entity_translation_admin_form($form, $form_state) {
$options = array();
$form['locale_field_language_fallback'] = array(
'#type' => 'checkbox',
'#title' => t('Enable language fallback'),
'#description' => t('When language fallback is enabled, if a translation is not available for the requested language an existing one will be displayed.'),
'#default_value' => variable_get('locale_field_language_fallback', TRUE),
);
$form['entity_translation_shared_labels'] = array(
'#type' => 'checkbox',
'#title' => t('Display shared labels'),
'#description' => t('Append an "all languages" hint to entity form widgets shared across translations.'),
'#default_value' => variable_get('entity_translation_shared_labels', TRUE),
);
$form['entity_translation_workflow_enabled'] = array(
'#type' => 'checkbox',
'#title' => t('Enable translation workflow permissions'),
'#description' => t('By enabling the translation workflow permissions it will be possible to limit the access to the entity form elements. Once this is active every role previously allowed to access the entity form will need to be granted the <em>Edit original values</em> permission to edit the entity in the original language. Moreover, form elements dealing with values shared across the translations will be visible only to roles having been granted the <em>Edit shared values</em> permission.'),
'#default_value' => entity_translation_workflow_enabled(),
);
$_null = NULL;
list($items, ) = menu_router_build();
_entity_translation_validate_path_schemes($_null, FALSE, $items);
foreach (entity_get_info() as $entity_type => $info) {
if ($info['fieldable']) {
$et_info = &$info['translation']['entity_translation'];
_entity_translation_process_path_schemes($entity_type, $et_info);
_entity_translation_validate_path_schemes($et_info['path schemes'], $info['label']);
// Translation can be enabled for the current entity only if it defines at
// least one valid base path.
foreach ($et_info['path schemes'] as $delta => $scheme) {
if (!empty($scheme['base path'])) {
$options[$entity_type] = !empty($info['label']) ? t($info['label']) : $entity_type;
break;
}
}
}
}
// Avoid bloating memory with unused data.
drupal_static_reset('_entity_translation_validate_path_schemes');
$enabled_types = array_filter(variable_get('entity_translation_entity_types', array()));
$form['enabled'] = array(
'#type' => 'fieldset',
'#title' => t('Translatable entity types'),
'#description' => t('Select which entities can be translated.'),
'#collapsible' => TRUE,
'#collapsed' => !empty($enabled_types),
);
$form['enabled']['entity_translation_entity_types'] = array(
'#type' => 'checkboxes',
'#options' => $options,
'#default_value' => $enabled_types,
);
$form['tabs'] = array(
'#type' => 'vertical_tabs',
);
$languages = array(
ENTITY_TRANSLATION_LANGUAGE_DEFAULT => t('Default language'),
ENTITY_TRANSLATION_LANGUAGE_CURRENT => t('Current language'),
ENTITY_TRANSLATION_LANGUAGE_AUTHOR => t('Author language'),
LANGUAGE_NONE => t('Language neutral'),
);
foreach (entity_translation_languages($entity_type) as $langcode => $language) {
$languages[$langcode] = t($language->name);
}
foreach ($enabled_types as $entity_type) {
$label = $options[$entity_type];
$entity_info = entity_get_info($entity_type);
$bundles = !empty($entity_info['bundles']) ? $entity_info['bundles'] : array($entity_type => array('label' => $label));
$enabled_bundles = 0;
foreach ($bundles as $bundle => $info) {
if (entity_translation_enabled_bundle($entity_type, $bundle)) {
$enabled_bundles++;
$settings = entity_translation_settings($entity_type, $bundle);
$settings_key = 'entity_translation_settings_' . $entity_type . '__' . $bundle;
$form['settings'][$entity_type][$settings_key] = array('#tree' => TRUE);
// If the entity defines no bundle we do not need the fieldset.
if (count($bundles) > 1 || $bundle != $entity_type) {
$form['settings'][$entity_type][$settings_key] += array(
'#type' => 'fieldset',
'#title' => $info['label'],
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
}
$form['settings'][$entity_type][$settings_key]['default_language'] = array(
'#type' => 'select',
'#title' => t('Default language'),
'#options' => $languages,
'#default_value' => $settings['default_language'],
);
$form['settings'][$entity_type][$settings_key]['hide_language_selector'] = array(
'#type' => 'checkbox',
'#title' => t('Hide language selector'),
'#default_value' => $settings['hide_language_selector'],
);
$form['settings'][$entity_type][$settings_key]['exclude_language_none'] = array(
'#type' => 'checkbox',
'#title' => t('Exclude <em>Language neutral</em> from the available languages'),
'#default_value' => $settings['exclude_language_none'],
);
$form['settings'][$entity_type][$settings_key]['lock_language'] = array(
'#type' => 'checkbox',
'#title' => t('Prevent language from being changed once the entity has been created'),
'#default_value' => $settings['lock_language'],
);
$form['settings'][$entity_type][$settings_key]['shared_fields_original_only'] = array(
'#type' => 'checkbox',
'#title' => t('Hide shared elements on translation forms'),
'#description' => t('Display form elements shared across translations only on entity forms for the original language.'),
'#default_value' => $settings['shared_fields_original_only'],
);
}
}
if ($enabled_bundles > 0) {
$form['settings'][$entity_type][$settings_key]['#collapsed'] = $enabled_bundles > 1;
$form['settings'][$entity_type] += array(
'#type' => 'fieldset',
'#group' => 'tabs',
'#title' => $label,
);
}
}
$form = system_settings_form($form);
// Menu rebuilding needs to be performed after the system settings are saved.
$form['#submit'][] = 'entity_translation_admin_form_submit';
return $form;
}
/**
* Submit handler for the entity translation settings form.
*/
function entity_translation_admin_form_submit($form, $form_state) {
// Clear the entity info cache for the new entity translation settings.
entity_info_cache_clear();
menu_rebuild();
}
/**
* Applies the given settings to every defined bundle.
*
* @param $entity_type
* The entity type the settings refer to.
* @param $settings
* (optional) The settings to be applied. Defaults to the entity default
* settings.
*/
function entity_translation_settings_init($entity_type, $settings = array()) {
if (entity_translation_enabled($entity_type)) {
$info = entity_get_info($entity_type);
$bundles = !empty($info['bundles']) ? array_keys($info['bundles']) : array($entity_type);
foreach ($bundles as $bundle) {
if (entity_translation_enabled_bundle($entity_type, $bundle)) {
$settings += entity_translation_settings($entity_type, $bundle);
}
}
variable_set('entity_translation_settings_' . $entity_type . '__' . $bundle, $settings);
}
}
/**
* Translations overview page callback.
*/
function entity_translation_overview($entity_type, $entity, $callback = NULL) {
// Entity translation and node translation share the same system path.
if ($callback && entity_translation_node($entity_type, $entity)) {
return entity_translation_overview_callback($callback, $entity);
}
$handler = entity_translation_get_handler($entity_type, $entity);
$handler->initPathScheme();
// Initialize translations if they are empty.
$translations = $handler->getTranslations();
if (empty($translations->original)) {
$handler->initTranslations();
$handler->saveTranslations();
}
// Ensure that we have a coherent status between entity language and field
// languages.
if ($handler->initOriginalTranslation()) {
// FIXME!
field_attach_presave($entity_type, $entity);
field_attach_update($entity_type, $entity);
}
$header = array(t('Language'), t('Source language'), t('Translation'), t('Status'), t('Operations'));
$languages = entity_translation_languages();
$source = $translations->original;
$path = $handler->getViewPath();
$rows = array();
if (drupal_multilingual()) {
// If we have a view path defined for the current entity get the switch
// links based on it.
if ($path) {
$links = EntityTranslationDefaultHandler::languageSwitchLinks($path);
}
foreach ($languages as $language) {
$classes = array();
$options = array();
$language_name = $language->name;
$langcode = $language->language;
$edit_path = $handler->getEditPath($langcode);
$add_path = "{$handler->getEditPath()}/add/$source/$langcode";
if ($edit_path) {
$add_links = EntityTranslationDefaultHandler::languageSwitchLinks($add_path);
$edit_links = EntityTranslationDefaultHandler::languageSwitchLinks($edit_path);
}
if (isset($translations->data[$langcode])) {
list(, , $bundle) = entity_extract_ids($entity_type, $entity);
// Existing translation in the translation set: display status.
$is_original = $langcode == $translations->original;
$translation = $translations->data[$langcode];
$label = _entity_translation_label($entity_type, $entity, $langcode);
$link = isset($links->links[$langcode]['href']) ? $links->links[$langcode] : array('href' => $path, 'language' => $language);
$row_title = l($label, $link['href'], $link);
if (empty($link['href'])) {
$row_title = $is_original ? $label : t('n/a');
}
if ($edit_path && $handler->getAccess('update') && $handler->getTranslationAccess($langcode)) {
$link = isset($edit_links->links[$langcode]['href']) ? $edit_links->links[$langcode] : array('href' => $edit_path, 'language' => $language);
$link['query'] = isset($_GET['destination']) ? drupal_get_destination() : FALSE;
$options[] = l(t('edit'), $link['href'], $link);
}
$status = $translation['status'] ? t('Published') : t('Not published');
$classes[] = $translation['status'] ? 'published' : 'not-published';
$status .= isset($translation['translate']) && $translation['translate'] ? theme('entity_translation_overview_outdated', array('message' => t('outdated'))) : '';
$classes[] = isset($translation['translate']) && $translation['translate'] ? 'outdated' : '';
if ($is_original) {
$language_name = t('<strong>@language_name</strong>', array('@language_name' => $language_name));
$source_name = t('(original content)');
}
else {
$source_name = $languages[$translation['source']]->name;
}
}
else {
// No such translation in the set yet: help user to create it.
$row_title = $source_name = t('n/a');
if ($source != $langcode && $handler->getAccess('update')) {
list(, , $bundle) = entity_extract_ids($entity_type, $entity);
$translatable = FALSE;
foreach (field_info_instances($entity_type, $bundle) as $instance) {
$field_name = $instance['field_name'];
$field = field_info_field($field_name);
if ($field['translatable']) {
$translatable = TRUE;
break;
}
}
$link = isset($add_links->links[$langcode]['href']) ? $add_links->links[$langcode] : array('href' => $add_path, 'language' => $language);
$link['query'] = isset($_GET['destination']) ? drupal_get_destination() : FALSE;
$options[] = $translatable ? l(t('add'), $link['href'], $link) : t('No translatable fields');
$classes[] = $translatable ? '' : 'non-traslatable';
}
$status = t('Not translated');
}
$rows[] = array(
'data' => array($language_name, $source_name, $row_title, $status, implode(" | ", $options)),
'class' => $classes,
);
}
}
drupal_set_title(t('Translations of %label', array('%label' => $handler->getLabel())), PASS_THROUGH);
// Add metadata to the build render allow to let other modules know about
// which entity this is.
$build['#entity_type'] = $entity_type;
$build['#entity'] = $entity;
$build['entity_translation_overview'] = array(
'#theme' => 'entity_translation_overview',
'#header' => $header,
'#rows' => $rows,
);
return $build;
}
/**
* Calls the appropriate translation overview callback.
*/
function entity_translation_overview_callback($callback, $entity) {
if (module_exists($callback['module'])) {
if (isset($callback['file'])) {
$path = isset($callback['file path']) ? $callback['file path'] : drupal_get_path('module', $callback['module']);
require_once DRUPAL_ROOT . '/' . $path . '/' . $callback['file'];
}
return $callback['page callback']($entity);
}
}
/**
* Returns the appropriate entity label for the given language.
*/
function _entity_translation_label($entity_type, $entity, $langcode = NULL) {
if (function_exists('title_entity_label')) {
list (, , $bundle) = entity_extract_ids($entity_type, $entity);
$entity_info = entity_get_info($entity_type);
if (!empty($entity_info['entity keys']['label'])) {
$legacy_field = $entity_info['entity keys']['label'];
if (title_field_replacement_enabled($entity_type, $bundle, $legacy_field)) {
$title = title_entity_label($entity, $entity_type, $langcode);
if (!empty($title)) {
return $title;
}
}
}
}
return t('view');
}
/**
* Theme wrapper for the entity translation language page.
*/
function theme_entity_translation_overview($variables){
$rows = $variables['rows'];
return theme('table', array(
'rows' => $rows,
'header' => $variables['header'],
));
}
/**
* Theme wrapper for the entity translation language outdated translation.
*/
function theme_entity_translation_overview_outdated($variables){
$message = $variables['message'];
return ' - <span class="marker">' . $message . '</span>';
}
/**
* Translation deletion confirmation form.
*/
function entity_translation_delete_confirm($form, $form_state, $entity_type, $entity, $langcode) {
$handler = entity_translation_get_handler($entity_type, $entity);
$languages = language_list();
$form = array(
'#handler' => $handler,
'#entity_type' => $entity_type,
'#entity' => $entity,
'#language' => $langcode,
);
return confirm_form(
$form,
t('Are you sure you want to delete the @language translation of %label?', array('@language' => $languages[$langcode]->name, '%label' => $handler->getLabel())),
$handler->getEditPath($langcode),
t('This action cannot be undone.'),
t('Delete'),
t('Cancel')
);
}
/**
* Submit handler for the translation deletion confirmation.
*/
function entity_translation_delete_confirm_submit($form, &$form_state) {
$handler = $form['#handler'];
$entity_type = $form['#entity_type'];
$entity = $form['#entity'];
$langcode = $form['#language'];
// Some modules expect the original values to be present when updating the
// field values. Since we are deleting the translation no value has changed.
$entity->original = $entity;
// Remove the translation entry and the related fields.
$handler->removeTranslation($langcode);
field_attach_presave($entity_type, $entity);
field_attach_update($entity_type, $entity);
$form_state['redirect'] = $handler->getTranslatePath();
}
/*
* Confirm form for changing field translatability.
*/
function entity_translation_translatable_form($form, &$form_state, $field_name) {
$field = field_info_field($field_name);
if ($field['translatable']) {
$title = t('Are you sure you want to disable translation for this field?');
$text = t('All occurrences of this field will become <em>untranslatable</em>:');
if (empty($form_state['input'])) {
drupal_set_message(t('All the existing field translations will be deleted. This operation cannot be undone.'), 'warning');
}
}
else {
$title = t('Are you sure you want to enable translation for this field?');
$text = t('All occurrences of this field will become <em>translatable</em>:');
$form['options'] = array(
'#type' => 'fieldset',
'#title' => t('Migration settings'),
'#weight' => -10,
);
$form['options']['copy_all_languages'] = array(
'#title' => t('Copy translations'),
'#description' => t('Copy field data into <em>all</em> existing translations, otherwise data will only be available in the original language.'),
'#type' => 'checkbox',
'#default_value' => TRUE,
);
}
$text .= _entity_translation_field_desc($field);
$text .= t('This operation may take a long time to complete.');
// We need to keep some information for later processing.
$form_state['field'] = $field;
// Store the 'translatable' status on the client side to prevent outdated form
// submits from toggling translatability.
$form['translatable'] = array(
'#type' => 'hidden',
'#default_value' => $field['translatable'],
);
return confirm_form($form, $title, '', $text);
}
/**
* Submit handler for the field settings form.
*
* This submit handler maintains consistency between the translatability of an
* entity and the language under which the field data is stored. When a field is
* marked as translatable, all the data in $entity->{field_name}[LANGUAGE_NONE]
* is moved to $entity->{field_name}[$entity_language]. When a field is marked
* as untranslatable the opposite process occurs. Note that marking a field as
* untranslatable will cause all of its translations to be permanently removed,
* with the exception of the one corresponding to the entity language.
*/
function entity_translation_translatable_form_submit($form, $form_state) {
// This is the current state that we want to reverse.
$translatable = $form_state['values']['translatable'];
$field_name = $form_state['field']['field_name'];
$copy_all_languages = !empty($form_state['values']['copy_all_languages']);
$field = field_info_field($field_name);
if ($field['translatable'] !== $translatable) {
// Field translatability has changed since form creation, abort.
$t_args = array('%field_name' => $field_name, '!translatable' => $translatable ? t('untranslatable') : t('translatable'));
drupal_set_message(t('The field %field_name is already !translatable. No change was performed.', $t_args), 'warning');
return;
}
// If a field is untranslatable, it can have no data except under
// LANGUAGE_NONE. Thus we need a field to be translatable before we convert
// data to the entity language. Conversely we need to switch data back to
// LANGUAGE_NONE before making a field untranslatable lest we lose
// information.
$operations = array(
array('entity_translation_translatable_batch', array(!$translatable, $field_name, $copy_all_languages)),
array('entity_translation_translatable_switch', array(!$translatable, $field_name)),
);
$operations = $translatable ? $operations : array_reverse($operations);
$t_args = array('%field' => $field_name);
$title = !$translatable ? t('Enabling translation for the %field field', $t_args) : t('Disabling translation for the %field field', $t_args);
$batch = array(
'title' => $title,
'operations' => $operations,
'finished' => 'entity_translation_translatable_batch_done',
'file' => drupal_get_path('module', 'entity_translation') . '/entity_translation.admin.inc',
);
batch_set($batch);
}
/*
* Toggle translatability of the given field.
*
* This is called from a batch operation, but should only run once per field.
*/
function entity_translation_translatable_switch($translatable, $field_name) {
$field = field_info_field($field_name);
if ($field['translatable'] === $translatable) {
return;
}
$field['translatable'] = $translatable;
field_update_field($field);
// This is needed for versions of Drupal core 7.10 and lower.
// See http://drupal.org/node/1380660 for details.
drupal_static_reset('field_available_languages');
}
/**
* Batch operation. Convert field data to or from LANGUAGE_NONE.
*/
function entity_translation_translatable_batch($translatable, $field_name, $copy_all_languages, &$context) {
if (empty($context['sandbox'])) {
$context['sandbox']['progress'] = 0;
// How many entities will need processing?
$query = new EntityFieldQuery();
$count = $query
->fieldCondition($field_name)
->count()
->execute();
if (intval($count) === 0) {
// Nothing to do.
$context['finished'] = 1;
return;
}
$context['sandbox']['max'] = $count;
}
// Number of entities to be processed for each step.
$limit = variable_get('entity_translation_translatable_batch_limit', 10);
$offset = $context['sandbox']['progress'];
$query = new EntityFieldQuery();
$result = $query
->fieldCondition($field_name)
->entityOrderBy('entity_id')
->range($offset, $limit)
->execute();
foreach ($result as $entity_type => $entities) {
foreach (entity_load($entity_type, array_keys($entities)) as $entity) {
$context['sandbox']['progress']++;
$handler = entity_translation_get_handler($entity_type, $entity);
$langcode = $handler->getLanguage();
// Skip process for language neutral entities.
if ($langcode == LANGUAGE_NONE) {
continue;
}
// We need a two-steps approach while updating field translations: given
// that field-specific update functions might rely on the stored values to
// perform their processing, see for instance file_field_update(), first
// we need to store the new translations and only after we can remove the
// old ones. Otherwise we might have data loss, since the removal of the
// old translations might occur before the new ones are stored.
if ($translatable && isset($entity->{$field_name}[LANGUAGE_NONE])) {
// If the field is being switched to translatable and has data for
// LANGUAGE_NONE then we need to move the data to the right language.
$translations = $handler->getTranslations();
if ($copy_all_languages && !empty($translations->data)) {
foreach ($translations->data as $translation) {
$entity->{$field_name}[$translation['language']] = $entity->{$field_name}[LANGUAGE_NONE];
}
}
else {
$entity->{$field_name}[$langcode] = $entity->{$field_name}[LANGUAGE_NONE];
}
// Store the original value.
_entity_translation_update_field($entity_type, $entity, $field_name);
$entity->{$field_name}[LANGUAGE_NONE] = array();
// Remove the language neutral value.
_entity_translation_update_field($entity_type, $entity, $field_name);
}
elseif (!$translatable && isset($entity->{$field_name}[$langcode])) {
// The field has been marked untranslatable and has data in the entity
// language: we need to move it to LANGUAGE_NONE and drop the other
// translations.
$entity->{$field_name}[LANGUAGE_NONE] = $entity->{$field_name}[$langcode];
// Store the original value.
_entity_translation_update_field($entity_type, $entity, $field_name);
// Remove translations.
foreach ($entity->{$field_name} as $langcode => $items) {
if ($langcode != LANGUAGE_NONE) {
$entity->{$field_name}[$langcode] = array();
}
}
_entity_translation_update_field($entity_type, $entity, $field_name);
}
else {
// No need to save unchanged entities.
continue;
}
}
}
$context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
}
/**
* Stores the given field translations.
*/
function _entity_translation_update_field($entity_type, $entity, $field_name) {
$empty = 0;
$field = field_info_field($field_name);
// Ensure that we are trying to store only valid data.
foreach ($entity->{$field_name} as $langcode => $items) {
$entity->{$field_name}[$langcode] = _field_filter_items($field, $entity->{$field_name}[$langcode]);
$empty += empty($entity->{$field_name}[$langcode]);
}
// Save the field value only if there is at least one item available,
// otherwise any stored empty field value would be deleted. If this happens
// the range queries would be messed up.
if ($empty < count($entity->{$field_name})) {
field_attach_presave($entity_type, $entity);
field_attach_update($entity_type, $entity);
}
}
/**
* Check the exit status of the batch operation.
*/
function entity_translation_translatable_batch_done($success, $results, $operations) {
if ($success) {
drupal_set_message(t("Data successfully processed."));
}
else {
// @todo: Do something about this case.
drupal_set_message(t("Something went wrong while processing data. Some nodes may appear to have lost fields."));
}
}

View File

@@ -0,0 +1,153 @@
<?php
/**
* @file
* API documentation for the Entity translation module.
*/
/**
* Allows modules to define their own translation info.
*
* Entity Translation relies on the core entity information to provide its
* translation features. See the documentation of hook_entity_info() in the core
* API documentation (system.api.php) for more details on all the entity info
* keys that may be defined.
*
* To make Entity Translation automatically support an entity type some keys
* may need to be defined, but none of them is required except the 'base path'
* key if the entity path is different from ENTITY_TYPE/%ENTITY_TYPE (e.g.
* taxonomy/term/1). The 'base path' key is used to determine the view, edit and
* translate path if they follow the default path patterns and to reliably
* alter menu information to provide the translation UI. If the entity path
* matches the default pattern above, and there is no need for a dedicated
* translation handler class, Entity Translation will provide built-in support
* for the entity.
*
* The entity translation info is an associative array that has to match the
* following structure. Three nested sub-arrays keyed respectively by entity
* type, the 'translation' key and the 'entity_translation' key: the second one
* is the key defined by the core entity system while the third one registers
* Entity translation as a field translation handler. Elements:
* - class: The name of the translation handler class, which is used to handle
* the translation process. Defaults to 'EntityTranslationDefaultHandler'.
* - base path: The base menu router path to which attach the administration
* user interface. Defaults to ENTITY_TYPE/%ENTITY_TYPE.
* - access callback: The access callback for the translation pages. Defaults to
* 'entity_translation_tab_access'.
* - access arguments: The access arguments for the translation pages. Defaults
* to "array($entity_type, $entity_position)".
* - view path: The menu router path to be used to view the entity. Defaults to
* the base path.
* - edit path: The menu router path to be used to edit the entity. Defaults to
* "$base_path/edit".
* - translate path: The menu router path to be used for attaching the
* translation UI. Defaults to "$base_path/translate".
* - path wildcard: The menu router path wildcard identifying the entity.
* Defaults to %ENTITY_TYPE.
* - admin theme: Whether the translation UI should use the administration
* theme. Defaults to TRUE.
* - path schemes: An array of menu router path schemes used for attaching the
* entity translation UI. This element can be used to declare additional path
* schemes, if an entity type uses multiple schemes for managing entities
* (e.g. different schemes for different bundles). Each path scheme can define
* the following elements (descriptions see above): 'base path', 'view path',
* 'edit path', 'translate path', 'path wildcard' and 'admin theme'. All path
* elements that are defined directly on the entity translation info array are
* automatically added as a 'default' path scheme.
* - theme callback: The callback to be used to determine the translation
* theme. Defaults to 'variable_get'.
* - theme arguments: The arguments to be used to determine the translation
* theme. Defaults to "array('admin_theme')".
* - edit form: The key to be used to retrieve the entity object from the form
* state array. An empty value prevents Entity translation from performing
* alterations to the entity form. Defaults to ENTITY_TYPE.
* - skip original values access: A flag specifying whether skipping access
* control when editing original values for this entity. Defaults to FALSE.
* - bundle callback: A callback to check whether the passed bundle has entity
* translation enabled. If empty all bundles are supposed to be enabled.
* - default settings: The defaults to be applied to settings when an explicit
* choice is missing.
*/
function hook_entity_info() {
$info['custom_entity'] = array(
'translation' => array(
'entity_translation' => array(
'class' => 'EntityTranslationCustomEntityHandler',
'base path' => 'custom_entity/%custom_entity',
'access callback' => 'custom_entity_tab_access',
'access arguments' => array(1),
'edit form' => 'custom_entity_form_state_key',
'bundle callback' => 'custom_entity_translation_enabled_bundle',
'default settings' => array(
'default_language' => LANGUAGE_NONE,
'hide_language_selector' => FALSE,
),
),
),
);
// Entity type which has multiple (e.g. bundle-specific) paths.
$info['custom_entity_2'] = array(
'translation' => array(
'entity_translation' => array(
'class' => 'EntityTranslationCustomEntityHandler',
'path sets' => array(
'default' => array(
'base path' => 'custom_entity_2/%custom_entity',
'path wildcard' => '%custom_entity',
),
'fancy' => array(
// Base path is not required.
'edit path' => 'fancy/%entity/edit',
'path wildcard' => '%entity',
),
),
)
)
);
return $info;
}
/**
* Allows modules to act when a new translation is added.
*
* @param $entity_type
* The entity type.
* @param $entity
* The entity.
* @param $translation
* The inserted translation array.
* @param $values
* The translated set of values, if any.
*/
function hook_entity_translation_insert($entity_type, $entity, $translation, $values = array()) {
}
/**
* Allows modules to act when a translation is updated.
*
* @param $entity_type
* The entity type.
* @param $entity
* The entity.
* @param $translation
* The updated translation array.
* @param $values
* The translated set of values, if any.
*/
function hook_entity_translation_update($entity_type, $entity, $translation, $values = array()) {
}
/**
* Allows modules to act when a translation is deleted.
*
* @param $entity_type
* The entity type.
* @param $entity
* The entity.
* @param $langcode
* The langcode of the translation which was deleted.
*/
function hook_entity_translation_delete($entity_type, $entity, $langcode) {
}

View File

@@ -0,0 +1,28 @@
name = Entity Translation
description = Allows entities to be translated into different languages.
package = Multilingual - Entity Translation
core = 7.x
configure = admin/config/regional/entity_translation
dependencies[] = locale (>7.14)
files[] = includes/translation.handler.inc
files[] = includes/translation.handler.comment.inc
files[] = includes/translation.handler.node.inc
files[] = includes/translation.handler.taxonomy_term.inc
files[] = includes/translation.handler.user.inc
files[] = tests/entity_translation.test
files[] = views/entity_translation_handler_relationship.inc
files[] = views/entity_translation_handler_field_translate_link.inc
files[] = views/entity_translation_handler_field_label.inc
files[] = views/entity_translation_handler_filter_entity_type.inc
files[] = views/entity_translation_handler_filter_language.inc
files[] = views/entity_translation_handler_filter_translation_exists.inc
; Information added by drupal.org packaging script on 2013-07-23
version = "7.x-1.0-beta3"
core = "7.x"
project = "entity_translation"
datestamp = "1374601567"

View File

@@ -0,0 +1,256 @@
<?php
/**
* @file
* Installation functions for Entity Translation module.
*/
/**
* Implements hook_schema().
*/
function entity_translation_schema() {
$schema['entity_translation'] = array(
'description' => 'Table to track entity translations',
'fields' => array(
'entity_type' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'The entity type this translation relates to',
),
'entity_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The entity id this translation relates to',
),
// @todo: Consider an integer field for 'language'.
'language' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
'description' => 'The target language for this translation.',
),
'source' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
'description' => 'The source language from which this translation was created.',
),
'uid' => array(
'description' => 'The author of this translation.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'status' => array(
'description' => 'Boolean indicating whether the translation is published (visible to non-administrators).',
'type' => 'int',
'not null' => TRUE,
'default' => 1,
),
'translate' => array(
'description' => 'A boolean indicating whether this translation needs to be updated.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'created' => array(
'description' => 'The Unix timestamp when the translation was created.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'changed' => array(
'description' => 'The Unix timestamp when the translation was most recently saved.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
'primary key' => array('entity_type', 'entity_id', 'language'),
);
return $schema;
}
/**
* Implements hook_install().
*/
function entity_translation_install() {
// entity_translation_form_alter() needs to run after locale_form_alter() and
// translation_menu(); entity_translation_menu_alter() needs to run after
// i18n_node_menu_alter().
db_update('system')
->fields(array('weight' => 11))
->condition('name', 'entity_translation')
->execute();
// Enable translation for nodes.
variable_set('entity_translation_entity_types', array('node' => 'node'));
// Make translation use the content language type.
variable_set('translation_language_type', LANGUAGE_TYPE_CONTENT);
}
/**
* Grant 'edit $type original values' permission to existing roles.
*/
function _entity_translation_grant_edit_permissions() {
variable_set('entity_translation_workflow_enabled', TRUE);
$permissions = array();
// Nodes.
$permissions['node'][] = 'bypass node access';
foreach (node_permissions_get_configured_types() as $type) {
$permissions['node'][] = "edit own $type content";
$permissions['node'][] = "edit any $type content";
}
// Comments.
if (module_exists('comment')) {
$permissions['comment'][] = 'administer comments';
$permissions['comment'][] = 'edit own comments';
}
// Taxonomy terms.
if (module_exists('taxonomy')) {
$permissions['taxonomy_term'][] = 'administer taxonomy';
foreach (taxonomy_get_vocabularies() as $vocabulary) {
$permissions['taxonomy_term'][] = "edit terms in {$vocabulary->vid}";
}
}
$assignments = array();
foreach ($permissions as $entity_type => $permissions_filter) {
if (entity_translation_enabled($entity_type)) {
$permission = "edit $entity_type original values";
$assignments[] = _entity_translation_grant_permission($permission, $permissions_filter);
$permission = "edit $entity_type translation shared fields";
$assignments[] = _entity_translation_grant_permission($permission, $permissions_filter);
}
}
$assignments = '<ul><li>' . implode('</li><li>', $assignments) . '</li></ul>';
$t = get_t();
return $t('The following permissions have been assigned to existing roles: !assignments', array('!assignments' => $assignments));
}
/**
* Grant the given permission to all roles which already have any of the
* permissions specified in the $permissions_filter parameter.
*
* @param $permission
* The new permission which to grant.
* @param $permissions_filter
* List of permissions used for loading roles.
*
* @return
* A message describing permission changes.
*/
function _entity_translation_grant_permission($permission, $permissions_filter = NULL) {
$roles = user_roles(FALSE, $permissions_filter);
foreach ($roles as $rid => $role) {
user_role_grant_permissions($rid, array($permission));
}
$t = get_t();
return $t('%permission was assigned to %roles', array(
'%permission' => $permission,
'%roles' => implode(', ', $roles)
));
}
/**
* Implements hook_enable().
*/
function entity_translation_enable() {
// Re-activate entity translation for content types which had used it when
// the module was last disabled (if any), unless these have since been altered
// by the user to use a different translation option.
$entity_translation_types = variable_get('entity_translation_disabled_content_types', array());
foreach ($entity_translation_types as $index => $type) {
if (variable_get("language_content_type_$type", 0) == 0) {
variable_set("language_content_type_$type", ENTITY_TRANSLATION_ENABLED);
}
// We should show the warning only if we actually restored at least one
// content type.
else {
unset($entity_translation_types[$index]);
}
}
if ($entity_translation_types) {
drupal_set_message(t('All content types previously configured to use field translation are now using it again.'), 'warning');
}
variable_del('entity_translation_disabled_content_types');
}
/**
* Implements hook_disable().
*/
function entity_translation_disable() {
// Store record of which types are using entity translation, and set those
// types to not be translated. These content types will be reset to use entity
// translation again if the module is later re-enabled, unless they have been
// changed by the user in the meantime.
$entity_translation_types = array();
foreach (node_type_get_types() as $type => $object) {
if (variable_get("language_content_type_$type", 0) == ENTITY_TRANSLATION_ENABLED) {
$entity_translation_types[] = $type;
variable_set("language_content_type_$type", 0);
}
}
if ($entity_translation_types) {
variable_set('entity_translation_disabled_content_types', $entity_translation_types);
drupal_set_message(t('All content types configured to use field translation now have multilingual support disabled. This change will be reverted if the entity translation module is enabled again.'), 'warning');
}
}
/**
* Implements hook_uninstall().
*/
function entity_translation_uninstall() {
db_delete('variable')
->condition('name', db_like('entity_translation_') . '%', 'LIKE')
->execute();
variable_del('translation_language_type');
variable_del('locale_field_language_fallback');
}
/**
* Implements hook_update_N().
*/
function entity_translation_update_7001() {
db_update('system')
->fields(array('weight' => 11))
->condition('name', 'entity_translation')
->execute();
}
/**
* Grant 'edit original values' and 'edit shared field' permissions to roles which have entity editing permissions.
*/
function entity_translation_update_7002() {
// Grant the 'edit original values' permission, so we don't break editing on
// existing sites.
return _entity_translation_grant_edit_permissions();
}
/**
* Configure node and comment language settings to the prior default behavior.
*/
function entity_translation_update_7003() {
module_load_include('inc', 'entity_translation', 'entity_translation.admin');
foreach (array_keys(entity_get_info()) as $entity_type) {
entity_translation_settings_init($entity_type);
}
}
/**
* Rebuild entity information to update the path scheme settings.
*/
function entity_translation_update_7004() {
entity_info_cache_clear();
}

View File

@@ -0,0 +1,20 @@
(function ($) {
Drupal.behaviors.translationNodeFieldsetSummaries = {
attach: function (context) {
$('fieldset#edit-translation', context).drupalSetSummary(function (context) {
var status = $('#edit-translation-status', context).is(':checked') ? Drupal.t('Translation published') : Drupal.t('Translation not published');
var translate;
if ($('#edit-translation-retranslate', context).size()) {
translate = $('#edit-translation-retranslate', context).is(':checked') ? Drupal.t('Flag translations as outdated') : Drupal.t('Do not flag translations as outdated');
}
else {
translate = $('#edit-translation-translate', context).is(':checked') ? Drupal.t('Needs to be updated') : Drupal.t('Does not need to be updated');
}
return status + ', ' + translate;
});
}
};
})(jQuery);

View File

@@ -0,0 +1,251 @@
<?php
/**
* @file
* The node specific translation functions and hook implementations.
*/
/**
* Identifies a content type which has translation support enabled.
*/
define('ENTITY_TRANSLATION_ENABLED', 4);
/**
* Hides translation metadata.
*/
define('ENTITY_TRANSLATION_METADATA_HIDE', 0);
/**
* Adds translation metadata to the original authoring information.
*/
define('ENTITY_TRANSLATION_METADATA_SHOW', 1);
/**
* Replaces the original authoring information with translation metadata.
*/
define('ENTITY_TRANSLATION_METADATA_REPLACE', 2);
/**
* Checks if the given entity has node translation enabled.
*/
function entity_translation_node($entity_type, $node) {
return $entity_type == 'node' && function_exists('translation_supported_type') && translation_supported_type($node->type);
}
/**
* Node-specific menu alterations.
*/
function entity_translation_node_menu_alter(&$items, $backup) {
if (isset($backup['node'])) {
$item = $backup['node'];
// Preserve the menu router item defined by other modules.
$callback['page callback'] = $item['page callback'];
$callback['file'] = $item['file'];
$callback['module'] = $item['module'];
$access_arguments = array_merge(array(1, $item['access callback']), $item['access arguments']);
}
else {
$callback = FALSE;
$access_arguments = array(1);
}
$items['node/%node/translate']['page callback'] = 'entity_translation_overview';
$items['node/%node/translate']['page arguments'] = array('node', 1, $callback);
$items['node/%node/translate']['access arguments'] = $access_arguments;
$items['node/%node/translate']['access callback'] = 'entity_translation_node_tab_access';
$items['node/%node/translate']['file'] = 'entity_translation.admin.inc';
$items['node/%node/translate']['module'] = 'entity_translation';
}
/**
* Node specific access callback.
*/
function entity_translation_node_tab_access() {
$args = func_get_args();
$node = array_shift($args);
if (entity_translation_node('node', $node)) {
$function = array_shift($args);
return call_user_func_array($function, $args);
}
else {
return entity_translation_tab_access('node', $node);
}
}
/**
* Returns whether the given node type has support for translations.
*
* @return
* Boolean value.
*/
function entity_translation_node_supported_type($type) {
return variable_get('language_content_type_' . $type, 0) == ENTITY_TRANSLATION_ENABLED;
}
/**
* Implements hook_node_view().
*
* Provides content language switcher links to navigate among node translations.
*/
function entity_translation_node_view($node, $build_mode, $langcode) {
if (!empty($node->translations) && drupal_multilingual() && entity_translation_node_supported_type($node->type) && !variable_get("entity_translation_hide_translation_links_{$node->type}", FALSE)) {
$path = 'node/' . $node->nid;
$links = EntityTranslationDefaultHandler::languageSwitchLinks($path);
if (is_object($links) && !empty($links->links)) {
$handler = entity_translation_get_handler('node', $node);
$translations = $handler->getTranslations()->data;
// Remove the link for the current language.
unset($links->links[$langcode]);
// Remove links to unavailable translations.
foreach ($links->links as $langcode => $link) {
if (!isset($translations[$langcode]) || !entity_translation_access('node', $translations[$langcode])) {
unset($links->links[$langcode]);
}
}
$node->content['links']['translation'] = array(
'#theme' => 'links',
'#links' => $links->links,
'#attributes' => array('class' => 'links inline'),
);
}
}
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Provides settings into the node content type form to choose for entity
* translation metadata and comment filtering.
*/
function entity_translation_form_node_type_form_alter(&$form, &$form_state) {
if (entity_translation_enabled('node')) {
$type = $form['#node_type']->type;
$t_args = array('!url' => url('admin/config/regional/entity_translation'));
$form['workflow']['language_content_type']['#options'][ENTITY_TRANSLATION_ENABLED] = t('Enabled, with field translation');
$form['workflow']['language_content_type']['#description'] .= ' <p>' . t('If field translation is selected you can have per-field translation for each available language. You can find more options in the <a href="!url">entity translation settings</a>.', $t_args) . '</p>';
// Hide settings when entity translation is disabled for this content type.
$states = array(
'visible' => array(
':input[name="language_content_type"]' => array('value' => ENTITY_TRANSLATION_ENABLED),
),
);
$form['workflow']['entity_translation_hide_translation_links'] = array(
'#type' => 'checkbox',
'#default_value' => variable_get("entity_translation_hide_translation_links_$type", FALSE),
'#title' => t('Hide content translation links'),
'#description' => t('Hide the links to translations in content body and teasers. If you choose this option, switching language will only be available from the language switcher block.'),
'#states' => $states,
);
$form['display']['entity_translation_node_metadata'] = array(
'#type' => 'radios',
'#title' => t('Translation post information'),
'#description' => t('Whether the translation authoring information should be hidden, shown, or replace the node\'s authoring information.'),
'#default_value' => variable_get("entity_translation_node_metadata_$type", ENTITY_TRANSLATION_METADATA_HIDE),
'#options' => array(t('Hidden'), t('Shown'), t('Replacing post information')),
'#states' => $states,
);
if (isset($form['comment'])) {
$form['comment']['entity_translation_comment_filter'] = array(
'#type' => 'checkbox',
'#title' => t('Filter comments per language'),
'#default_value' => variable_get("entity_translation_comment_filter_$type", FALSE),
'#description' => t('Show only comments whose language matches content language.'),
'#states' => $states,
);
}
}
}
/**
* Implements hook_preprocess_node().
*
* Alters node template variables to show/replace entity translation metadata.
*/
function entity_translation_preprocess_node(&$variables) {
$node = $variables['node'];
$submitted = variable_get("node_submitted_{$node->type}", TRUE);
$mode = variable_get("entity_translation_node_metadata_{$node->type}", ENTITY_TRANSLATION_METADATA_HIDE);
if ($submitted && $mode != ENTITY_TRANSLATION_METADATA_HIDE) {
global $language_content, $user;
$handler = entity_translation_get_handler('node', $node);
$translations = $handler->getTranslations();
$langcode = $language_content->language;
if (isset($translations->data[$langcode]) && $langcode != $translations->original) {
$translation = $translations->data[$langcode];
$date = format_date($translation['created']);
$name = FALSE;
if ($node->uid != $translation['uid']) {
$account = $user->uid != $translation['uid'] ? user_load($translation['uid']) : $user;
$name = theme('username', array('account' => $account));
}
switch ($mode) {
case ENTITY_TRANSLATION_METADATA_SHOW:
$variables['date'] .= ' (' . t('translated on <em>!date</em>', array('!date' => $date)) . ')';
if ($name) {
$variables['name'] .= ' (' . t('translated by !name', array('!name' => $name)) . ')';
}
break;
case ENTITY_TRANSLATION_METADATA_REPLACE:
$variables['date'] = $date;
if ($name) {
$variables['name'] = $name;
}
break;
}
}
}
}
/**
* Returns whether the given comment type has support for translations.
*
* @return
* Boolean value.
*/
function entity_translation_comment_supported_type($comment_type) {
$type = str_replace('comment_node_', '', $comment_type);
return entity_translation_node_supported_type($type);
}
/**
* Implements hook_query_TAG_alter().
*
* Filters out node comments by content language.
*
* @todo Find a way to track node comment statistics per language.
*/
function entity_translation_query_comment_filter_alter(QueryAlterableInterface $query) {
$node = $query->getMetaData('node');
if (!empty($node->type) && variable_get("entity_translation_comment_filter_{$node->type}", FALSE)) {
// Determine alias for "comment" table.
$comment_alias = FALSE;
foreach ($query->getTables() as $table) {
if (is_string($table['table']) && $table['table'] == 'comment') {
$comment_alias = $table['alias'];
break;
}
}
// Only show comments without language or matching the current content language.
if ($comment_alias) {
$query->condition(db_or()
->condition($comment_alias . '.language', $GLOBALS['language_content']->language)
->condition($comment_alias . '.language', LANGUAGE_NONE)
);
}
}
}

View File

@@ -0,0 +1,79 @@
<?php
/**
* @file
* This file provides the rules integration for this module.
*/
/**
* Implements of hook_rules_event_info().
*/
function entity_translation_rules_event_info() {
$events = array(
'entity_translation_insert' => array(
'label' => t('After adding an entity translation'),
'group' => t('Entity'),
'variables' => array(
'entity_type' => array(
'type' => 'text',
'label' => t('Entity type'),
),
'entity' => array(
'type' => 'entity',
'label' => t('Entity'),
),
'language_code' => array(
'type' => 'text',
'label' => t('Language code'),
),
'values' => array(
'type' => 'list',
'label' => t('Raw values'),
'optional' => TRUE,
),
),
),
'entity_translation_update' => array(
'label' => t('After updating an entity translation'),
'group' => t('Entity'),
'variables' => array(
'entity_type' => array(
'type' => 'text',
'label' => t('Entity type'),
),
'entity' => array(
'type' => 'entity',
'label' => t('Entity'),
),
'language_code' => array(
'type' => 'text',
'label' => t('Language code'),
),
'values' => array(
'type' => 'list',
'label' => t('Raw values'),
'optional' => TRUE,
),
),
),
'entity_translation_delete' => array(
'label' => t('After deleting an entity translation'),
'group' => t('Entity'),
'variables' => array(
'entity_type' => array(
'type' => 'text',
'label' => t('Entity type'),
),
'entity' => array(
'type' => 'entity',
'label' => t('Entity'),
),
'language_code' => array(
'type' => 'text',
'label' => t('Language code'),
),
),
),
);
return $events;
}

View File

@@ -0,0 +1,15 @@
name = Entity Translation Menu
description = Allows menu items to be translated on the entity form.
package = Multilingual - Entity Translation
core = 7.x
dependencies[] = entity_translation
dependencies[] = i18n
dependencies[] = i18n_menu
files[] = entity_translation_i18n_menu.test
; Information added by drupal.org packaging script on 2013-07-23
version = "7.x-1.0-beta3"
core = "7.x"
project = "entity_translation"
datestamp = "1374601567"

View File

@@ -0,0 +1,408 @@
<?php
/**
* @file
* The menu specific translation functions and hook implementations.
*/
/**
* Implements hook_node_prepare().
*
* Translates the menu item shown on node edit forms if the node language does
* not equal the language of the menu item. This means either loading the
* respective menu item from the translation set or localizing the item.
*/
function entity_translation_i18n_menu_node_prepare($node) {
$langcode = entity_language('node', $node);
if (!empty($langcode) && !empty($node->menu['language']) && $node->menu['language'] != $langcode && entity_translation_i18n_menu_item($node->menu)) {
$handler = entity_translation_get_handler('node', $node);
$source_langcode = $handler->getSourceLanguage();
// If we are creating a translation we need to use the source language.
entity_translation_i18n_menu_node_menu_item_translate($node, $source_langcode ? $source_langcode : $langcode);
}
}
/**
* Implements hook_module_implements_alter().
*/
function entity_translation_i18n_menu_module_implements_alter(&$implementations, $hook) {
switch ($hook) {
case 'node_prepare':
case 'node_presave':
// Move some of our hook implementations to end of list. Required so that
// the 'menu' key is populated when our implementation gets called. This
// also prevents our changes from being overridden.
$group = $implementations['entity_translation_i18n_menu'];
unset($implementations['entity_translation_i18n_menu']);
$implementations['entity_translation_i18n_menu'] = $group;
break;
}
}
/**
* Implements hook_node_presave().
*/
function entity_translation_i18n_menu_node_presave($node) {
if (!entity_translation_enabled('node')) {
return;
}
$handler = entity_translation_get_handler('node', $node);
$translations = $handler->getTranslations();
$source_langcode = $handler->getSourceLanguage();
$tset = !empty($node->menu['tset']);
// If no translation is available the menu data is always supposed to be
// entered in the source string language. This way we avoid having unneeded
// string translations hanging around.
if (empty($source_langcode) && count($translations->data) < 2 && !$tset) {
return;
}
// When creating a new translation, leave the source menu item intact and
// create a new one.
$langcode = entity_language('node', $node);
if (!empty($node->menu) && $tset && !empty($source_langcode)) {
$node->source_menu = menu_link_load($node->menu['mlid']);
unset($node->menu['mlid']);
}
// Store the entity language for later reference when saving a translation.
// If we are editing a translation in the string source language, we can skip
// item processing since the proper values are already in place. Instead when
// creating the translation we need to process the link item before saving it.
if (!empty($node->menu) && !empty($langcode) && ($source_langcode || $langcode != i18n_string_source_language())) {
$node->menu['entity_language'] = $langcode;
$node->menu['entity_translation_handler'] = $handler;
}
// If we have a translation set here we should prepare it for storage,
// otherwise we need to ensure the menu item has no language so it can be
// localized.
if ($tset) {
entity_translation_i18n_menu_item_tset_prepare($node, $langcode);
}
else {
$node->menu['language'] = LANGUAGE_NONE;
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function entity_translation_i18n_menu_form_menu_edit_item_alter(&$form, &$form_state) {
$form['#validate'][] = 'entity_translation_i18n_menu_form_menu_edit_item_validate';
}
/**
* Implements hook_menu_link_alter().
*/
function entity_translation_i18n_menu_menu_link_alter(&$link) {
if (!empty($link['mlid']) && !empty($link['entity_language']) && $link['language'] == LANGUAGE_NONE && entity_translation_i18n_menu_item($link)) {
$sources = array();
foreach (array('title' => 'link_title', 'description' => 'description') as $key => $link_field) {
$name = array('menu', 'item', $link['mlid'], $key);
$source = i18n_string_get_source($name);
// The source might not exist yet.
$sources[$key] = is_object($source) ? $source->get_string() : $link[$link_field];
}
// If the link values to be saved are translated, we need to revert the
// localized menu link back to the original. This way they can be saved
// without accidentially storing a translation string as a source string.
// The translated values are put in a separate key for later reference.
if ($link['entity_language'] != i18n_string_source_language()) {
$link['entity_translation_strings'] = array(
'title' => $link['link_title'],
'description' => $link['description'],
);
$link['link_title'] = $sources['title'];
$link['options']['attributes']['title'] = $sources['description'];
}
// If the link values are in the string source language, we need to save
// the previous source values as translations. As a matter of fact this can
// happen only when initially creating a menu item in a language different
// from the source string one.
else {
$link['entity_translation_strings'] = array(
'title' => $sources['title'],
'description' => $sources['description'],
);
$link['entity_language'] = $link['entity_translation_handler']->getLanguage();
}
}
}
/**
* Implements hook_menu_link_update().
*/
function entity_translation_i18n_menu_menu_link_update($link) {
// Make sure localizations are saved properly.
if (entity_translation_i18n_menu_item($link) && !empty($link['entity_translation_strings'])) {
$string_langcode = isset($link['entity_language']) ? $link['entity_language'] : i18n_string_source_language();
$name = implode(':', array('menu', 'item', $link['mlid']));
foreach ($link['entity_translation_strings'] as $key => $translation) {
i18n_string_translation_update($name . ':' . $key, $translation, $string_langcode);
}
}
}
/**
* Menu specific alterations for the entity form.
*
* Adds to the regular menu item widget a checkbox to choose whether the current
* menu item should be localized or part of a translation set.
*/
function entity_translation_i18n_menu_form(&$form, &$form_state) {
$info = entity_translation_edit_form_info($form, $form_state);
if ($info && $info['entity type'] == 'node') {
$node = $info['entity'];
$source_menu = isset($node->source_menu) ? $node->source_menu : $node->menu;
// Check that the menu item of the source node is translatable.
if (isset($form['menu']) && !empty($source_menu) && i18n_menu_mode($source_menu['menu_name'], I18N_MODE_MULTIPLE)) {
$default = isset($source_menu['language']) && $source_menu['language'] != LANGUAGE_NONE;
$languages = language_list();
$handler = entity_translation_entity_form_get_handler($form, $form_state);
$langcode = $handler->getFormLanguage();
$language_name = isset($languages[$langcode]) ? t($languages[$langcode]->name) : t('current');
$form['menu']['#multilingual'] = TRUE;
$form['menu']['link']['tset'] = array(
'#type' => 'checkbox',
'#title' => t('Menu link enabled only for the %language language', array('%language' => $language_name)),
'#prefix' => '<label>' . t('Menu translation') . '</label>',
'#default_value' => $default,
'#description' => t('Create a different menu link for each translation. Every link will have its own parent and weight, otherwise only title and description will be translated.'),
'#weight' => 10,
);
if (!empty($default)) {
$translation_set = i18n_menu_translation_load($source_menu['i18n_tsid']);
$translations = $translation_set ? $translation_set->get_translations() : FALSE;
if (!empty($translations) && (count($translations) > 1 || !isset($translations[$langcode]))) {
$form['menu']['link']['tset']['#disabled'] = TRUE;
}
}
}
}
}
/**
* Validation handler for the menu item edit form.
*/
function entity_translation_i18n_menu_form_menu_edit_item_validate($form, &$form_state) {
$item = $form_state['values'];
// Localizable menu items should not be created when a translation set for the
// same path already exists.
if ($item['language'] == LANGUAGE_NONE) {
$count = db_select('menu_links', 'ml')
->condition('ml.link_path', $item['link_path'])
->condition('ml.i18n_tsid', 0, '<>')
->countQuery()
->execute()
->fetchField();
if (!empty($count)) {
form_set_error('language', t('There are already one or more items with a language assigned for the given path. Remove them or assign a language to this item too.'));
}
}
}
/**
* Checks whether a given menu item is translatable through entity translation.
*
* @param array $item
* A menu item.
*
* @todo
* Find more generic way of determining whether ET is enabled for a link; add
* support for other entities, e.g. taxonomy_term (?).
*/
function entity_translation_i18n_menu_item($item) {
$cache = &drupal_static(__FUNCTION__, array());
if (!isset($cache[$item['link_path']])) {
// First check that the item belongs to a menu which has translation
// enabled.
if (!i18n_menu_mode($item['menu_name'], I18N_MODE_MULTIPLE)) {
$cache[$item['link_path']] = FALSE;
}
// Check if the respective node type has entity translation enabled.
if (preg_match('!^node/(\d+)(/.+|)$!', $item['link_path'], $matches)) {
if (!entity_translation_enabled('node')) {
$cache[$item['link_path']] = FALSE;
}
else {
$type = db_select('node', 'n')
->condition('nid', $matches[1])
->fields('n', array('type'))
->execute()->fetchField();
$cache[$item['link_path']] = entity_translation_node_supported_type($type);
}
}
else {
$cache[$item['link_path']] = FALSE;
}
}
return $cache[$item['link_path']];
}
/**
* Replace the menu item on the given node with a localized version.
*
* If the menu item is replaced by a different menu item from the translation
* set, the original item is stored in $node->source_menu.
*
* @param $node
* A node object, with a menu item ($node->menu).
* @param $langcode
* The language into which the menu item should be translated.
*/
function entity_translation_i18n_menu_node_menu_item_translate($node, $langcode) {
// Localization.
if ($node->menu['language'] == LANGUAGE_NONE) {
_i18n_menu_link_localize($node->menu, $langcode);
// Update properties 'link_title' and 'options.attributes.title' which are
// used for the node menu form; i18n_menu_link_localize only localizes
// rendered properties 'title' and 'localized_options.attributes.title'.
$node->menu['link_title'] = $node->menu['title'];
$node->menu['options']['attributes']['title'] = isset($node->menu['localized_options']['attributes']['title']) ? $node->menu['localized_options']['attributes']['title'] : '';
}
// Translation sets.
else {
$menu = NULL;
if (!empty($node->menu['i18n_tsid']) && $translation_set = i18n_menu_translation_load($node->menu['i18n_tsid'])) {
// Load menu item from translation set.
$menu = $translation_set->get_item($langcode);
// Set parent_depth_limit (required on node forms).
if (!empty($menu) && !isset($menu['parent_depth_limit'])) {
$menu['parent_depth_limit'] = _menu_parent_depth_limit($menu);
}
// Make sure the menu item is not set to hidden; i18n_menu automatically
// hides any menu items not matching the current interface language.
if (!empty($menu)) {
$menu['hidden'] = FALSE;
}
}
// Replace the menu item with the translated version, or null if there is
// no translated item. Store the original one in $node->source_menu.
$node->source_menu = $node->menu;
$node->menu = $menu;
}
}
/**
* Prepares the menu item attached to given entity for saving.
*
* - Ensures that different menu items attached to the entity and its
* translations are stored within the same translation set.
* - Sets missing default values, and cleans out null values.
* - Sets the language of the menu item to given target language.
*
* @param $entity
* Node object.
* @param $langcode
* Target language.
*/
function entity_translation_i18n_menu_item_tset_prepare($entity, $langcode) {
// Load or create a translation set.
if (!empty($entity->source_menu)) {
if (!empty($entity->source_menu['i18n_tsid'])) {
$translation_set = i18n_translation_set_load($entity->source_menu['i18n_tsid']);
}
else {
// Make sure that the source menu item does have a language assigned.
if ($entity->source_menu['language'] == LANGUAGE_NONE) {
$entity->source_menu['language'] = $entity->menu['entity_translation_handler']->getSourceLanguage();
menu_link_save($entity->source_menu);
}
// Create new translation set.
$translation_set = i18n_translation_set_build('menu_link')
->add_item($entity->source_menu);
}
$entity->menu['translation_set'] = $translation_set;
}
// Extract menu_name and pid from parent property.
if (!empty($entity->menu['parent'])) {
list($entity->menu['menu_name'], $entity->menu['plid']) = explode(':', $entity->menu['parent']);
}
// Remove null values.
$entity->menu = array_filter($entity->menu);
$entity->menu['language'] = $langcode;
$entity->menu += array(
'description' => '',
'customized' => 1,
);
}
/**
* Implements hook_entity_translation_upgrade().
*/
function entity_translation_i18n_menu_entity_translation_upgrade($node, $translation) {
menu_node_prepare($node);
menu_node_prepare($translation);
if (!empty($node->menu['mlid']) && !empty($translation->menu['mlid'])) {
$link = $node->menu;
$link['link_title'] = $translation->menu['link_title'];
$link['description'] = $translation->menu['description'];
$link['entity_language'] = $translation->language;
$link['language'] = LANGUAGE_NONE;
menu_link_save($link, $node->menu);
}
}
/**
* Implements hook_entity_translation_delete().
*/
function entity_translation_i18n_menu_entity_translation_delete($entity_type, $entity, $langcode) {
// Make sure that we are working with an entity of type node.
if ($entity_type != 'node') {
return;
}
list($entity_id, , ) = entity_extract_ids($entity_type, $entity);
// Clean-up all menu module links that point to this node.
$result = db_select('menu_links', 'ml')
->fields('ml', array('mlid', 'language'))
->condition('link_path', 'node/' . $entity_id)
->condition('module', 'menu')
->execute()
->fetchAllAssoc('mlid');
foreach ($result as $link) {
// Delete all menu links matching the deleted language.
if ($link->language == $langcode) {
menu_link_delete($link->mlid);
}
// Delete string translations for all language-neutral menu items.
if ($link->language == LANGUAGE_NONE) {
$name = array('menu', 'item', $link->mlid);
foreach (array('title', 'description') as $key) {
$name[] = $key;
$source = i18n_string_get_source($name);
if(!empty($source->lid)) {
db_delete('locales_target')
->condition('lid', $source->lid)
->condition('language', $langcode)
->execute();
}
}
}
}
}

View File

@@ -0,0 +1,255 @@
<?php
/**
* @file
* Tests for Entity translation module.
*/
/**
* Tests for the translation of menu items on entity forms.
*/
class EntityTranslationMenuTranslationTestCase extends EntityTranslationTestCase {
/**
* Return the test information.
*/
public static function getInfo() {
return array(
'name' => 'Menu translation',
'description' => 'Tests for the translation of menu items on entity forms.',
'group' => 'Entity translation',
'dependencies' => array('i18n_menu'),
);
}
function setUp() {
parent::setUp('locale', 'entity_translation', 'i18n_menu', 'entity_translation_i18n_menu');
$this->login($this->getAdminUser(array('administer menu')));
$this->addLanguage('en');
$this->addLanguage('es');
$this->addLanguage('it');
$this->configureContentType();
$this->configureMenu();
$this->enableUrlLanguageDetection();
$this->login($this->getTranslatorUser(array('administer menu')));
}
/**
* Configure the "Main Menu" for multilingual menu items ("Translate & Localize").
*/
function configureMenu() {
$edit = array();
$edit['i18n_mode'] = I18N_MODE_MULTIPLE; // Translate & Localize.
$this->drupalPost('admin/structure/menu/manage/main-menu/edit', $edit, t('Save'));
$this->assertRaw(t('Your configuration has been saved.'), t('Menu settings have been saved.'));
}
/**
* Create page with menu item.
*/
function createPage($link_title, $description, $langcode) {
$edit = array();
$edit['title'] = $this->randomName();
$edit['language'] = $langcode;
$edit['menu[enabled]'] = TRUE;
$edit['menu[link_title]'] = $link_title;
$edit['menu[description]'] = $description ? $description : 'link language = ' . $langcode;
$this->drupalPost('node/add/page', $edit, t('Save'));
$this->assertRaw(t('Basic page %title has been created.', array('%title' => $edit['title'])), t('Basic page created.'));
// Check to make sure the node was created.
$node = $this->drupalGetNodeByTitle($edit['title']);
$this->assertTrue($node, t('Node found in database.'));
// Check to make sure menu link was created.
$this->get($langcode, '<front>');
$this->assertText($link_title, 'New menu link found.');
return $node;
}
/**
* Create a translation with menu item.
*/
function createTranslation($node, $link_title, $description, $langcode, $use_tsets = FALSE) {
$this->drupalGet('node/' . $node->nid . '/edit/add/' . $node->language . '/' . $langcode);
$edit = array();
$edit['menu[enabled]'] = TRUE;
$edit['menu[link_title]'] = $link_title;
$edit['menu[description]'] = $description ? $description : 'link language = ' . $langcode;
if ($use_tsets != NULL) {
$edit['menu[tset]'] = $use_tsets;
}
$this->drupalPost(NULL, $edit, t('Save'));
$this->drupalGet('node/' . $node->nid . '/translate');
$this->assertLinkByHref('node/' . $node->nid . '/edit/' . $langcode, 0, t('Translation edit link found. Translation created.'));
// Check to make sure menu link was created.
$this->get($langcode, '<front>');
$this->assertText($link_title, 'Translation menu link found.');
return $node;
}
/**
* Remove translation in given langcode from node.
*/
function removeTranslation($node, $langcode) {
$this->drupalGet('node/' . $node->nid . '/translate/delete/' . $langcode);
$this->drupalPost(NULL, array(), t('Delete'));
// Check to make sure translation was deleted.
$this->drupalGet('node/' . $node->nid . '/edit/add/' . $node->language . '/' . $langcode);
$this->assertResponse(200, 'Translation add page found. Old translation deleted.');
}
/**
* Edit a page menu item.
*
* Check that node form contains old menu link title, then replace with given
* new title.
*/
function editPage($node, $old_link_title, $link_title, $langcode) {
$this->drupalGet('node/' . $node->nid . '/edit/' . $langcode);
$this->assertFieldByXPath("//input[@name='menu[link_title]']", $old_link_title, 'Old link title value correctly populated: ' . $old_link_title);
$edit = array();
$edit['menu[link_title]'] = $link_title;
$this->drupalPost(NULL, $edit, t('Save'));
// Check to make sure menu link was updated.
$this->get($langcode, '<front>');
$this->assertNoText($old_link_title, 'Old menu link title not found: ' . $old_link_title);
$this->assertText($link_title, 'New menu link title found: ' . $link_title);
}
/**
* Test if menu localization works.
*/
function testMenuLocalization() {
// Create Basic page in English.
$link_title_en = $this->randomName();
$node = $this->createPage($link_title_en, NULL, 'en');
// Submit translation in Spanish.
$link_title_es = $this->randomName();
$node_translation = $this->createTranslation($node, $link_title_es, NULL, 'es');
// Check menu links in both languages.
$this->get('en', '<front>');
$this->assertText($link_title_en);
$this->get('es', '<front>');
$this->assertText($link_title_es);
// Edit English menu link.
$link_title_en2 = $this->randomName();
$this->editPage($node, $link_title_en, $link_title_en2, 'en');
// Check that Spanish menu link has not changed.
$this->get('es', '<front>');
$this->assertText($link_title_es);
// Edit Spanish menu link.
$link_title_es2 = $this->randomName();
$this->editPage($node, $link_title_es, $link_title_es2, 'es');
// Check that English menu link has not changed.
$this->get('en', '<front>');
$this->assertText($link_title_en2);
// Delete Spanish translation and check that the respective menu item has
// been deleted as well.
$this->removeTranslation($node, 'es');
$this->get('es', '<front>');
$this->assertNoText($link_title_es2);
}
/**
* Test if menu localization works (source language != default language).
*/
function testMenuLocalizationCustomSourceLanguage() {
// Create Basic page in Spanish.
$link_title_es = $this->randomName();
$node = $this->createPage($link_title_es, NULL, 'es');
// Submit translation in English.
$link_title_en = $this->randomName();
$node_translation = $this->createTranslation($node, $link_title_en, NULL, 'en');
// Check menu links in both languages.
$this->get('es', '<front>');
$this->assertText($link_title_es);
$this->get('en', '<front>');
$this->assertText($link_title_en);
// Edit Spanish menu link.
$link_title_es2 = $this->randomName();
$this->editPage($node, $link_title_es, $link_title_es2, 'es');
// Check that English menu link has not changed.
$this->get('en', '<front>');
$this->assertText($link_title_en);
// Edit English menu link.
$link_title_en2 = $this->randomName();
$this->editPage($node, $link_title_en, $link_title_en2, 'en');
// Check that Spanish menu link has not changed.
$this->get('es', '<front>');
$this->assertText($link_title_es2);
}
/**
* Test if menu translation works with separate menu items.
*/
function testMenuTranslation() {
// Create Basic page in English.
$link_title_en = $this->randomName();
$node = $this->createPage($link_title_en, NULL, 'en');
// Submit translation in Spanish.
$link_title_es = $this->randomName();
$node_translation = $this->createTranslation($node, $link_title_es, NULL, 'es', TRUE);
// Check menu links in both languages.
$this->get('en', '<front>');
$this->assertText($link_title_en);
$this->get('es', '<front>');
$this->assertText($link_title_es);
// Edit English menu link.
$link_title_en2 = $this->randomName();
$this->editPage($node, $link_title_en, $link_title_en2, 'en');
// Check that Spanish menu link has not changed.
$this->get('es', '<front>');
$this->assertText($link_title_es);
// Edit Spanish menu link.
$link_title_es2 = $this->randomName();
$this->editPage($node, $link_title_es, $link_title_es2, 'es');
// Check that English menu link has not changed.
$this->get('en', '<front>');
$this->assertText($link_title_en2);
// Add another translation, and check that other menu items are not
// affected. See https://drupal.org/node/1982140
$link_title_it = $this->randomName();
$node_translation = $this->createTranslation($node, $link_title_it, NULL, 'it', NULL);
// Check that Spanish and English menu links have not changed.
$this->get('es', '<front>');
$this->assertText($link_title_es2);
$this->get('en', '<front>');
$this->assertText($link_title_en2);
// Delete Spanish translation and check that the respective menu item has
// been deleted as well.
$this->removeTranslation($node, 'es');
$this->get('es', '<front>');
$this->assertNoText($link_title_es2);
}
}

View File

@@ -0,0 +1,248 @@
<?php
/**
* @file
* Converts node translations into field-based translations.
*/
/**
* The number of node translations that should be processed for each batch step.
*/
define('ENTITY_TRANSLATION_UPGRADE_BATCH_SIZE', 10);
/**
* Starts the batch process to perform the upgrade.
*/
function entity_translation_upgrade_start($types) {
$batch = array(
'operations' => array(
array('entity_translation_upgrade_do', array($types)),
array('entity_translation_upgrade_complete', array()),
),
'finished' => 'entity_translation_upgrade_end',
'title' => t('Entity Translation Upgrade'),
'init_message' => t('Entity Translation Upgrade is starting.'),
'error_message' => t('Entity Translation Upgrade has encountered an error.'),
'file' => drupal_get_path('module', 'entity_translation_upgrade') . '/entity_translation_upgrade.admin.inc',
);
batch_set($batch);
batch_process('admin/config/regional/entity_translation');
}
/**
* Finshed batch callback.
*/
function entity_translation_upgrade_end($success, $results, $operations, $elapsed) {
if (!empty($results)) {
$message = format_plural($results, '1 node translation successfully upgraded.', '@count node translations successfully upgraded.');
watchdog('entity translation upgrade', '@count node translations successfully upgraded.', array('@count' => $results), WATCHDOG_INFO);
}
else {
$message = t('No node translation available for the upgrade.');
}
drupal_set_message($message);
}
/**
* Batch process to convert node translations to field-based translations.
*/
function entity_translation_upgrade_do($types, &$context) {
$query = db_select('node', 'n');
$query->addJoin('LEFT', 'entity_translation_upgrade_history', 'etuh', 'n.nid = etuh.nid');
$query
->fields('n', array('nid', 'tnid'))
->where('etuh.complete IS NULL OR etuh.complete = 0')
->condition('n.tnid', 0, '<>')
->condition('n.tnid <> n.nid', array(), '')
->condition('n.type', $types)
->orderBy('n.nid');
// Initialize the batch process.
if (empty($context['sandbox'])) {
$total = $query
->countQuery()
->execute()
->fetchField();
$context['sandbox']['count'] = 0;
$context['sandbox']['total'] = $total;
$context['finished'] = $total == 0;
}
else {
$batch_size = variable_get('entity_translation_upgrade_batch_size', ENTITY_TRANSLATION_UPGRADE_BATCH_SIZE);
$result = $query
->range($context['sandbox']['count'], $batch_size)
->execute()
->fetchAllKeyed();
// Here we load original nodes and translations all together, but the batch
// size is determined only by node translations.
$nids = array_keys($result);
$nodes = node_load_multiple($nids + array_unique($result));
$updated_nodes = array();
$node_translations = array();
$node_translation_sets = array();
$instances = array();
$field_info = array();
foreach ($nids as $nid) {
$node = $nodes[$nid];
$original = $nodes[$node->tnid];
$handler = entity_translation_get_handler('node', $original);
if (!isset($instances[$node->type])) {
$instances[$node->type] = field_info_instances('node', $node->type);
}
reset($instances[$node->type]);
foreach ($instances[$node->type] as $instance) {
$field_name = $instance['field_name'];
$field = isset($field_info[$field_name]) ? $field_info[$field_name] : $field_info[$field_name] = field_info_field($field_name);
// Copy field data.
if ($field['translatable']) {
$langcode = isset($node->{$field_name}[$node->language]) ? $node->language : LANGUAGE_NONE;
if (isset($node->{$field_name}[$langcode])) {
$original->{$field_name}[$node->language] = $node->{$field_name}[$langcode];
}
}
}
// Add the new translation.
$handler->setTranslation(array(
'translate' => $node->translate,
'status' => $node->status,
'language' => $node->language,
'source' => $original->language,
'uid' => $node->uid,
'created' => $node->created,
'changed' => $node->changed,
));
// Build a list of updated nodes. They will be saved after all the node
// translation conversions.
$updated_nodes[$original->nid] = $original;
// Build a list of obsolete node translations to be unpublished.
$node_translations[$node->nid] = $node;
// Build a list of obsolete translations sets to be passed to module hook
// implementations.
$node_translation_sets[$original->nid][$node->nid] = $node;
$context['sandbox']['count']++;
}
// Ensure that the multilingual support configuration is set to the right
// value for the current node type.
foreach ($instances as $type_name => $data) {
variable_set("language_content_type_$type_name", ENTITY_TRANSLATION_ENABLED);
}
// Save field data and translations for the updated nodes.
foreach ($updated_nodes as $nid => $node) {
field_attach_presave('node', $node);
field_attach_update('node', $node);
entity_translation_get_handler('node', $node)->saveTranslations();
foreach ($node_translation_sets[$nid] as $translation) {
// Allow modules to upgrade their node additions, if possible.
module_invoke_all('entity_translation_upgrade', $node, $translation);
}
}
if (!empty($node_translations)) {
$nids = array_keys($node_translations);
// Unpublish the obsolete node translations.
db_update('node')
->fields(array('status' => 0))
->condition('nid', $nids)
->execute();
db_update('node_revision')
->fields(array('status' => 0))
->condition('nid', $nids)
->execute();
// Populate history table.
$columns = array('nid', 'tnid', 'language');
$query = db_insert('entity_translation_upgrade_history')->fields($columns);
foreach ($node_translations as $node) {
$query->values((array) $node);
}
$query->execute();
// Flush the modified nodes from the entity load cache.
entity_get_controller('node')->resetCache($nids);
}
$context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total'];
if ($context['finished'] >= 1) {
$context['results'] = $context['sandbox']['total'];
}
}
}
/**
* Removes the translation sets for all the upgraded nodes.
*/
function entity_translation_upgrade_complete(&$context) {
if ($nids = db_query('SELECT DISTINCT etuh.tnid FROM {entity_translation_upgrade_history} etuh WHERE etuh.complete = 0')->fetchCol()) {
// Remove translation sets for migrated nodes.
db_query('UPDATE {node} SET tnid = 0 WHERE nid IN (:nids)', array(':nids' => $nids));
entity_get_controller('node')->resetCache($nids);
// Mark nodes as migrated.
db_query('UPDATE {entity_translation_upgrade_history} SET complete = 1 WHERE complete = 0');
}
}
/**
* Implementations of hook_entity_translation_upgrade() on behalf of core
* modules.
*/
/**
* Implements hook_entity_translation_upgrade().
*/
function path_entity_translation_upgrade($node, $translation) {
// Update URL aliases.
db_update('url_alias')
->fields(array('source' => 'node/' . $node->nid))
->condition('source', 'node/' . $translation->nid)
->execute();
}
/**
* Implements hook_entity_translation_upgrade().
*/
function comment_entity_translation_upgrade($node, $translation) {
// Attach comments to the original node.
db_update('comment')
->fields(array('nid' => $node->nid, 'language' => $translation->language))
->condition('nid', $translation->nid)
->execute();
// Update node-comment statistics.
$ncs = db_select('node_comment_statistics', 'ncs')
->fields('ncs')
->condition('nid', array($node->nid, $translation->nid))
->execute()
->fetchAllAssoc('nid');
$last = $ncs[$node->nid]->last_comment_timestamp > $ncs[$translation->nid]->last_comment_timestamp;
$ncs_updated = $last ? $ncs[$node->nid] : $ncs[$translation->nid];
$ncs_updated->nid = $node->nid;
$ncs_updated->comment_count = $ncs[$node->nid]->comment_count + $ncs[$translation->nid]->comment_count;
db_update('node_comment_statistics')
->fields((array) $ncs_updated)
->condition('nid', $node->nid)
->execute();
}

View File

@@ -0,0 +1,12 @@
name = Entity Translation Upgrade
description = Provides an upgrade path from node-based translation to field-based translation.
package = Multilingual - Entity Translation
core = 7.x
dependencies[] = entity_translation
; Information added by drupal.org packaging script on 2013-07-23
version = "7.x-1.0-beta3"
core = "7.x"
project = "entity_translation"
datestamp = "1374601567"

View File

@@ -0,0 +1,81 @@
<?php
/**
* @file
* Schema declaration for the entity_translation_upgrade module.
*/
/**
* Implements hook_schema().
*/
function entity_translation_upgrade_schema() {
$schema['entity_translation_upgrade_history'] = array(
'description' => 'The history table for node translations.',
'fields' => array(
'nid' => array(
'description' => 'The node translation nid.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
'tnid' => array(
'description' => 'The translation set id for the node translation.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
'language' => array(
'description' => 'The node translation language.',
'type' => 'varchar',
'length' => 12,
'not null' => TRUE,
'default' => '',
),
'complete' => array(
'description' => 'Boolean indicating whether the node migration has completed.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
'indexes' => array('tnid' => array('tnid'), 'complete' => array('complete')),
'primary key' => array('nid'),
);
return $schema;
}
/**
* Implements hook_enable().
*/
function entity_translation_upgrade_enable() {
$args = array('!url' => url('admin/config/regional/entity_translation'));
drupal_set_message(t('<em>Entity Translation Upgrade</em> enabled: visit the <a href="!url">entity translation settings</a> page to perform the upgrade.', $args));
}
/**
* Implements hook_update_N().
*
* Adds the 'complete' column to the history table.
*/
function entity_translation_upgrade_update_7001() {
$table = 'entity_translation_upgrade_history';
$name = 'complete';
// Add the 'complete' field.
$spec = array(
'description' => 'Boolean indicating whether the node migration has completed.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
);
db_add_field($table, $name, $spec);
// Add the 'complete' index.
db_add_index($table, $name, array($name));
// Existing records are supposed to concern already completed migrations.
db_update($table)
->fields(array('complete' => 1))
->execute();
}

View File

@@ -0,0 +1,141 @@
<?php
/**
* @file
* Provides permanent redirects for unavailable node translations.
*/
/**
* Implements hook_menu().
*/
function entity_translation_upgrade_menu() {
return array(
'admin/config/regional/entity_translation/upgrade' => array(
'title' => 'Entity Translation Upgrade',
'page callback' => 'drupal_get_form',
'page arguments' => array('entity_translation_upgrade_form'),
'access arguments' => array('administer software updates'),
'file' => 'entity_translation_upgrade.admin.inc',
'type' => MENU_CALLBACK,
),
);
}
/**
* Implements hook_menu_alter().
*/
function entity_translation_upgrade_menu_alter(&$items) {
// Obsolete node translations might be left unpublished instead of being
// deleted.
$items['node/%node']['access callback'] = 'entity_translation_upgrade_access';
$items['node/%node']['access arguments'] = array(1);
}
/**
* Access callback.
*
* Performs a redirect to the corresponding field-based translation if the
* current user has not the permission to access the requested node translation.
*/
function entity_translation_upgrade_access($node) {
// If the user has the right to access the node, we need to do nothing.
if (node_access('view', $node)) {
return TRUE;
}
// If we have a node translation, we need to redirect the user to the original
// node.
if ($node->tnid && $node->nid != $node->tnid) {
entity_translation_upgrade_redirect($node->tnid, $node->language);
}
return FALSE;
}
/**
* Implements hook_init().
*/
function entity_translation_upgrade_init() {
// If have a node/$nid path but we are not able to load a node for the given
// nid we might have an upgraded translation, hence we need to look for a
// record matching the requested nid in the history table.
if ($nid = entity_translation_upgrade_check_path() && $data = entity_translation_upgrade_load($nid)) {
entity_translation_upgrade_redirect($data->tnid, $data->language);
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function entity_translation_upgrade_form_entity_translation_admin_form_alter(&$form, $form_state) {
$form['entity_translation_upgrade'] = array(
'#type' => 'fieldset',
'#title' => t('Entity Translation Upgrade'),
'#description' => t('This will create an entity translation for each available node translation, which will be then unpublished.'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$options = array();
foreach (node_type_get_types() as $type) {
$options[$type->type] = $type->name;
}
$form['entity_translation_upgrade']['types'] = array(
'#type' => 'checkboxes',
'#title' => t('Node types'),
'#description' => t('Select which node types will be upgraded.'),
'#options' => $options,
);
$form['entity_translation_upgrade']['upgrade'] = array(
'#type' => 'submit',
'#value' => t('Upgrade'),
'#validate' => array('entity_translation_upgrade_validate'),
'#submit' => array('entity_translation_upgrade_submit'),
);
}
/**
* Validation handler for the entity_translation_admin_form() form.
*/
function entity_translation_upgrade_validate($form, &$form_state) {
if (!count(array_filter($form_state['values']['types']))) {
form_set_error('types', t('Please specify at least one node type.'));
}
}
/**
* Submit handler for the entity_translation_admin_form() form.
*/
function entity_translation_upgrade_submit($form, &$form_state) {
module_load_include('inc', 'entity_translation_upgrade', 'entity_translation_upgrade.admin');
entity_translation_upgrade_start(array_filter($form_state['values']['types']));
}
/**
* Performs the redirect to original node with the given language.
*/
function entity_translation_upgrade_redirect($nid, $langcode) {
$languages = language_list();
drupal_goto("node/$nid", array('language' => $languages[$langcode]), 301);
}
/**
* Checks wether the requested path belongs to an upgraded translation.
*/
function entity_translation_upgrade_check_path() {
$result = arg(0) == 'node' && ($nid = arg(1)) && is_int($nid) && !node_load($nid);
return $result ? $nid : FALSE;
}
/**
* Loads the upgrade history entry for the given nid.
*/
function entity_translation_upgrade_load($nid) {
return db_select('entity_translation_upgrade_history', 'etu')
->fields('etu')
->condition('etu.nid', $nid)
->execute()
->fetchObject();
}

View File

@@ -0,0 +1,47 @@
<?php
/**
* @file
* Comment translation handler for the entity translation module.
*/
/**
* Comment translation handler.
*/
class EntityTranslationCommentHandler extends EntityTranslationDefaultHandler {
public function __construct($entity_type, $entity_info, $entity) {
parent::__construct('comment', $entity_info, $entity);
}
/**
* @see EntityTranslationDefaultHandler::entityForm()
*/
public function entityForm(&$form, &$form_state) {
parent::entityForm($form, $form_state);
// Adjust the translation fieldset weight to move it below the admin one.
$form['translation']['#weight'] = 1;
}
/**
* @see EntityTranslationDefaultHandler::entityFormLanguageWidgetSubmit()
*/
public function entityFormLanguageWidgetSubmit($form, &$form_state) {
$this->updateFormLanguage($form_state);
}
/**
* @see EntityTranslationDefaultHandler::entityFormTitle()
*/
protected function entityFormTitle() {
return t('Edit comment @subject', array('@subject' => $this->getLabel()));
}
/**
* @see EntityTranslationDefaultHandler::getStatus()
*/
protected function getStatus() {
return (boolean) $this->entity->status;
}
}

View File

@@ -0,0 +1,118 @@
<?php
/**
* @file
* Node translation handler for the entity translation module.
*/
/**
* Node translation handler.
*
* Overrides default behaviours for Node properties.
*/
class EntityTranslationNodeHandler extends EntityTranslationDefaultHandler {
public function __construct($entity_type, $entity_info, $entity) {
parent::__construct('node', $entity_info, $entity);
}
/**
* @see EntityTranslationDefaultHandler::isRevision()
*/
public function isRevision() {
return !empty($this->entity->revision);
}
/**
* @see EntityTranslationDefaultHandler::getAccess()
*/
public function getAccess($op) {
return node_access($op, $this->entity);
}
/**
* @see EntityTranslationDefaultHandler::getTranslationAccess()
*/
public function getTranslationAccess($langcode) {
return user_access('bypass node access') || parent::getTranslationAccess($langcode);
}
/**
* Convert the translation update status fieldset into a vartical tab.
*/
public function entityForm(&$form, &$form_state) {
parent::entityForm($form, $form_state);
// Move the translation fieldset to a vertical tab.
if (isset($form['translation'])) {
$form['translation'] += array(
'#group' => 'additional_settings',
'#weight' => 100,
'#attached' => array(
'js' => array(drupal_get_path('module', 'entity_translation') . '/entity_translation.node-form.js'),
),
);
if (!$this->isTranslationForm()) {
$form['translation']['name']['#access'] = FALSE;
$form['translation']['created']['#access'] = FALSE;
}
}
// Path aliases natively support multilingual values.
if (isset($form['path'])) {
$form['path']['#multilingual'] = TRUE;
}
}
/**
* @see EntityTranslationDefaultHandler::menuForm()
*/
protected function menuForm(&$form, &$form_state) {
entity_translation_i18n_menu_form($form, $form_state);
}
/**
* @see EntityTranslationDefaultHandler::entityFormLanguageWidgetSubmit()
*/
function entityFormLanguageWidgetSubmit($form, &$form_state) {
$this->updateFormLanguage($form_state);
}
/**
* @see EntityTranslationDefaultHandler::entityFormSubmit()
*/
public function entityFormSubmit($form, &$form_state) {
if (!isset($form_state['values']['translation'])) {
// Always publish the original values when we have no translations.
$form_state['values']['translation'] = array('status' => TRUE);
}
$values = &$form_state['values']['translation'];
if (!$this->isTranslationForm()) {
// Inherit entity authoring information for the original values.
$values['name'] = $form_state['values']['name'];
if (!empty($form_state['values']['date'])) {
$values['created'] = $form_state['values']['date'];
}
}
parent::entityFormSubmit($form, $form_state);
}
/**
* @see EntityTranslationDefaultHandler::entityFormTitle()
*/
protected function entityFormTitle() {
$type_name = node_type_get_name($this->entity);
return t('<em>Edit @type</em> @title', array('@type' => $type_name, '@title' => $this->getLabel()));
}
/**
* @see EntityTranslationDefaultHandler::getStatus()
*/
protected function getStatus() {
return (boolean) $this->entity->status;
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* @file
* Taxonomy term translation handler for the entity translation module.
*/
/**
* Taxonomy term translation handler.
*/
class EntityTranslationTaxonomyTermHandler extends EntityTranslationDefaultHandler {
public function __construct($entity_type, $entity_info, $entity) {
parent::__construct('taxonomy_term', $entity_info, $entity);
}
/**
* @see EntityTranslationDefaultHandler::entityForm()
*/
public function entityForm(&$form, &$form_state) {
parent::entityForm($form, $form_state);
// Remove the translation fieldset when the deletion confirm form is being
// displayed.
if (isset($form_state['confirm_delete'])) {
unset(
$form[$this->getLanguageKey()],
$form['source_language'],
$form['translation'],
$form['actions']['delete_translation']
);
}
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* @file
* User translation handler for the entity translation module.
*/
/**
* User translation handler.
*/
class EntityTranslationUserHandler extends EntityTranslationDefaultHandler {
public function __construct($entity_type, $entity_info, $entity) {
parent::__construct('user', $entity_info, $entity);
}
/**
* @see EntityTranslationDefaultHandler::entityForm()
*/
public function entityForm(&$form, &$form_state) {
parent::entityForm($form, $form_state);
$form['picture']['#multilingual'] = FALSE;
$form['locale']['#multilingual'] = FALSE;
$form['locale']['#title'] = t('Preferred language settings');
}
/**
* @see EntityTranslationDefaultHandler::getLanguageKey()
*/
public function getLanguageKey() {
return 'entity_language';
}
}

View File

@@ -0,0 +1,526 @@
<?php
/**
* @file
* Tests for Entity translation module.
*/
/**
* Base class for entity translation module tests.
*/
class EntityTranslationTestCase extends DrupalWebTestCase {
protected $current_user;
protected $admin_user;
protected $translator_user;
function setUp() {
$args = func_get_args();
call_user_func_array(array('parent', 'setUp'), $args);
// Reset user fields to make test object reusable.
unset($this->current_user);
unset($this->admin_user);
unset($this->translator_user);
}
/**
* Retrieves a Drupal path or an absolute path with language.
*
* @param $language
* Language code or language object.
*/
function get($language, $path = '', array $options = array(), array $headers = array()) {
$options['language'] = $this->getLanguage($language);
return $this->drupalGet($path, $options, $headers);
}
/**
* Posts to a Drupal path with language.
*/
function post($language, $path, $edit, $submit, array $options = array(), array $headers = array(), $form_html_id = NULL, $extra_post = NULL) {
$options['language'] = $this->getLanguage($language);
$this->drupalPost($path, $edit, $submit, $options, $headers, $form_html_id, $extra_post);
}
/**
* Login the given user only if she has not changed.
*/
function login($user) {
if (!isset($this->current_user) || $this->current_user->uid != $user->uid) {
$this->current_user = $user;
$this->drupalLogin($user);
}
}
/**
* Returns a user with administration rights.
*
* @param $permissions
* Additional permissions for administrative user.
*/
function getAdminUser(array $permissions = array()) {
if (!isset($this->admin_user)) {
$this->admin_user = $this->drupalCreateUser(array_merge(array(
'bypass node access',
'administer nodes',
'administer languages',
'administer content types',
'administer blocks',
'access administration pages',
'administer site configuration',
), $permissions));
}
return $this->admin_user;
}
/**
* Returns a user with minimal translation rights.
*
* @param $permissions
* Additional permissions for administrative user.
*/
function getTranslatorUser(array $permissions = array()) {
if (!isset($this->translator_user)) {
$this->translator_user = $this->drupalCreateUser(array_merge(array(
'create page content',
'edit own page content',
'delete own page content',
'translate any entity',
), $permissions));
}
return $this->translator_user;
}
/**
* Make sure the clean urls are enabled.
*/
function enableCleanUrls() {
$this->drupalGet('admin/config/search/clean-urls');
$edit = array();
$edit['clean_url'] = TRUE;
$this->drupalPost(NULL, $edit, t('Save configuration'));
}
/**
* Enable URL language detection.
*/
function enableUrlLanguageDetection() {
// Enable URL language detection and selection.
$edit = array(
'language[enabled][locale-url]' => TRUE,
'language_content[enabled][locale-interface]' => TRUE
);
$this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings'));
$this->assertRaw(t('Language negotiation configuration saved.'), t('URL language detection enabled.'));
$this->drupalGet('admin/config/regional/language/configure');
// Reset caches.
drupal_static_reset('locale_url_outbound_alter');
drupal_static_reset('language_list');
}
/**
* Get a language object from a language code.
*/
public function getLanguage($langcode) {
if (is_object($langcode)) {
return $langcode;
}
else {
$language_list = language_list();
return $language_list[$langcode];
}
}
/**
* Install a specified language if it has not been already, otherwise make sure that the language is enabled.
*
* @param $langcode
* The language code to check.
*/
function addLanguage($langcode) {
// Check to make sure that language has not already been installed.
$this->drupalGet('admin/config/regional/language');
if (strpos($this->drupalGetContent(), 'enabled[' . $langcode . ']') === FALSE) {
// Doesn't have language installed so add it.
$edit = array();
$edit['langcode'] = $langcode;
$this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
// Make sure we are not using a stale list.
drupal_static_reset('language_list');
$languages = language_list('language');
$this->assertTrue(array_key_exists($langcode, $languages), t('Language was installed successfully.'));
if (array_key_exists($langcode, $languages)) {
$this->assertRaw(t('The language %language has been created and can now be used. More information is available on the <a href="@locale-help">help screen</a>.', array('%language' => $languages[$langcode]->name, '@locale-help' => url('admin/help/locale'))), t('Language has been created.'));
}
}
elseif ($this->xpath('//input[@type="checkbox" and @name=:name and @checked="checked"]', array(':name' => 'enabled[' . $langcode . ']'))) {
// It is installed and enabled. No need to do anything.
$this->assertTrue(TRUE, 'Language [' . $langcode . '] already installed and enabled.');
}
else {
// It is installed but not enabled. Enable it.
$this->assertTrue(TRUE, 'Language [' . $langcode . '] already installed.');
$this->drupalPost(NULL, array('enabled[' . $langcode . ']' => TRUE), t('Save configuration'));
$this->assertRaw(t('Configuration saved.'), t('Language successfully enabled.'));
}
}
/**
* Configure the "Basic page" content type for entity translation tests.
*/
function configureContentType() {
// Configure the "Basic page" content type to use multilingual support with
// translation.
$edit = array();
$edit['language_content_type'] = ENTITY_TRANSLATION_ENABLED;
$this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type'));
$this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Basic page')), t('Basic page content type has been updated.'));
// Set body field's cardinality to unlimited and toggle translatability.
$edit = array();
$edit['field[cardinality]'] = FIELD_CARDINALITY_UNLIMITED;
$edit['field[translatable]'] = 1;
$this->drupalPost('admin/structure/types/manage/page/fields/body', $edit, t('Save settings'));
$this->assertRaw(t('Saved %field configuration.', array('%field' => 'Body')), t('Body field settings have been updated.'));
// Check if the setting works.
$this->drupalGet('node/add/page');
$this->assertFieldById('edit-body-und-add-more', t('Add another item'), t('Add another item button found.'));
}
/**
* Create a "Basic page" in the specified language.
*
* @param $title
* Title of the basic page in the specified language.
* @param $body
* Body of the basic page in the specified language.
* @param $langcode
* The language code to be assigned to the specified values.
*/
function createPage($title, $body, $langcode) {
$edit = array();
$language_none = LANGUAGE_NONE;
$edit["title"] = $title;
$edit["body[$language_none][0][value]"] = $body;
$edit['language'] = $langcode;
$this->drupalPost('node/add/page', $edit, t('Save'));
$this->assertRaw(t('Basic page %title has been created.', array('%title' => $title)), t('Basic page created.'));
// Check to make sure the node was created.
$node = $this->drupalGetNodeByTitle($title);
$this->assertTrue($node, t('Node found in database.'));
return $node;
}
/**
* Create a translation.
*
* @param $node
* Node of the basic page to create translation for.
* @param $title
* Title of the basic page in the specified language.
* @param $body
* Body of the basic page in the specified language.
* @param $langcode
* The language code to be assigned to the specified values.
*/
function createTranslation($node, $title, $body, $langcode, $source_langcode = 'en') {
$this->drupalGet('node/' . $node->nid . '/edit/add/' . $source_langcode . '/' . $langcode);
$body_key = "body[$langcode][0][value]";
$this->assertFieldByXPath("//textarea[@name='$body_key']", $node->body[$node->language][0]['value'], 'Original body value correctly populated.');
$this->assertFieldById('edit-body-' . $langcode . '-add-more', t('Add another item'), t('Add another item button found.'));
$edit = array();
$edit[$body_key] = $body;
$this->drupalPost(NULL, $edit, t('Save'));
$this->drupalGet('node/' . $node->nid . '/translate');
$this->assertLinkByHref('node/' . $node->nid . '/edit/' . $langcode, 0, t('Translation edit link found. Translation created.'));
return $node;
}
}
/**
* Basic tests for the translation creation/editing workflow.
*/
class EntityTranslationTranslationTestCase extends EntityTranslationTestCase {
/**
* Return the test information.
*/
public static function getInfo() {
return array(
'name' => 'Entity translation workflow',
'description' => 'Basic tests for the translation creation/editing workflow.',
'group' => 'Entity translation',
);
}
function setUp() {
parent::setUp('locale', 'entity_translation');
$this->login($this->getAdminUser());
$this->addLanguage('en');
$this->addLanguage('es');
$this->configureContentType();
$this->login($this->getTranslatorUser());
}
/**
* Test if field based translation works.
*
* Enable field based translation for basic pages. Add a field with a
* cardinality higher than 1, to test if field_default_extract_form_values()
* is invoked. Create a basic page and translate it.
*/
function testFieldTranslation() {
// Create Basic page in English.
$node_title = $this->randomName();
$node_body = $this->randomName();
$node = $this->createPage($node_title, $node_body, 'en');
// Submit translation in Spanish.
$node_translation_title = $this->randomName();
$node_translation_body = $this->randomName();
$node_translation = $this->createTranslation($node, $node_translation_title, $node_translation_body, 'es');
}
}
/**
* Basic tests for comment related things.
*
* @todo Add tests for comment translation workflow.
*/
class EntityTranslationCommentTestCase extends EntityTranslationTestCase {
protected $comment_user;
/**
* Return the test information.
*/
public static function getInfo() {
return array(
'name' => 'Comment translation',
'description' => 'Basic tests for comment translation/filtering.',
'group' => 'Entity translation',
);
}
function setUp() {
parent::setUp('locale', 'entity_translation', 'comment');
$this->login($this->getAdminUser());
$this->addLanguage('en');
$this->addLanguage('es');
$this->enableUrlLanguageDetection();
$this->configureContentType();
$this->configureComments(FALSE);
$this->login($this->getTranslatorUser());
}
function tearDown() {
unset($this->comment_user);
parent::tearDown();
}
function getCommentUser() {
if (empty($this->comment_user)) {
$this->comment_user = $this->drupalCreateUser(array(
'access comments',
'post comments',
'edit own comments',
));
}
return $this->comment_user;
}
/**
* Enable comments and comment filtering by language.
*/
function configureComments($filter_by_language = TRUE) {
$edit = array();
$edit['comment'] = COMMENT_NODE_OPEN;
$edit['entity_translation_comment_filter'] = $filter_by_language;
$this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type'));
$this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Basic page')));
$this->drupalGet('admin/structure/types/manage/page');
if ($filter_by_language) {
$this->assertFieldChecked('edit-entity-translation-comment-filter', 'Comment filtering is enabled.');
}
else {
$this->assertNoFieldChecked('edit-entity-translation-comment-filter', 'Comment filtering is disabled.');
}
}
/**
* Add a comment for the given node.
*
* @param $node
* The node for which to add the comment.
* @param $comment_body
* The comment body text.
* @param $language
* The comment language.
*/
function postComment($node, $comment_body, $language) {
$edit = array();
$edit['comment_body[' . LANGUAGE_NONE . '][0][value]'] = $comment_body;
$this->post($language, 'comment/reply/' . $node->nid, $edit, t('Save'));
}
/**
* Test comment filtering by language.
*/
function testCommentLanguageFiltering() {
$node = $this->createPage($this->randomName(), $this->randomName(), 'en');
$this->login($this->getCommentUser());
// Create comments in different languages.
$comment_en = $this->randomName();
$this->postComment($node, $comment_en, 'en');
$comment_es = $this->randomName();
$this->postComment($node, $comment_es, 'es');
// Check that ALL comments are being displayed when comment language filter
// is disabled (default behavior).
$this->get('en', 'node/' . $node->nid);
$this->assertText($comment_en, 'English comment found.');
$this->assertText($comment_es, 'Spanish comment found.');
// Enable comment filtering by language.
$this->login($this->getAdminUser());
$this->configureComments(TRUE);
$this->login($this->getCommentUser());
// Load page in different languages. Check that only comments matching
// current language are being displayed.
$this->get('en', 'node/' . $node->nid);
$this->assertText($comment_en, 'English comment found.');
$this->assertNoText($comment_es, 'Spanish comment not found.');
$this->get('es', 'node/' . $node->nid);
$this->assertNoText($comment_en, 'English comment not found.');
$this->assertText($comment_es, 'Spanish comment found.');
}
}
/**
* Test CRUD hook invocation.
*/
class EntityTranslationHookTestCase extends EntityTranslationTestCase {
/**
* Return the test information.
*/
public static function getInfo() {
return array(
'name' => 'Entity translation hooks',
'description' => 'Test that entity translation hooks are properly fired.',
'group' => 'Entity translation',
);
}
function setUp() {
parent::setUp('locale', 'entity_translation', 'entity_translation_test');
$this->login($this->getAdminUser());
$this->addLanguage('it');
$this->addLanguage('es');
$this->configureContentType();
$this->login($this->getTranslatorUser());
}
/**
* Test whether hooks are properly fired in the regular form workflow.
*/
function testFormWorkflow() {
// Create Basic page in English.
$node_title = $this->randomName();
$node_body = $this->randomName();
$node = $this->createPage($node_title, $node_body, 'en');
// Submit translation in Italian.
$node_translation_body = $this->randomName();
$this->createTranslation($node, NULL, $node_translation_body, 'it');
$info = $this->getHookInfo();
$this->assertTrue(!empty($info['insert']), t('Insert hook has been properly fired.'));
// Edit translation in Italian.
$edit = array("body[it][0][value]" => $this->randomName());
$this->drupalPost('node/' . $node->nid . '/edit/it', $edit, t('Save'));
$info = $this->getHookInfo();
$this->assertTrue(!empty($info['update']), t('Update hook has been properly fired.'));
// Delete the Basic page.
$edit = array();
$this->drupalPost('node/' . $node->nid . '/delete', $edit, t('Delete'));
$info = $this->getHookInfo('delete');
$this->assertTrue(count($info) == 2 && !empty($info['en']) && !empty($info['it']), t('Delete hook has been properly fired.'));
}
/**
* Test whether hooks are properly fired when using the API.
*/
function testAPI() {
// Create Basic page in English.
$node = $this->createPage($this->randomName(), $this->randomName(), 'en');
$handler = entity_translation_get_handler('node', $node);
// Create a translation in Italian.
$translation = array('source' => 'en', 'language' => 'it');
$handler->setTranslation($translation);
$handler->saveTranslations();
$node = node_load($node->nid, NULL, TRUE);
$handler = entity_translation_get_handler('node', $node, TRUE);
$translations = $handler->getTranslations();
$this->assertTrue(!empty($translations->data['it']), t('An Italian translation has been created'));
$info = $this->getHookInfo();
$this->assertTrue(!empty($info['insert']), t('Insert hook has been properly fired.'));
// Check that the update hook is properly fired.
$translation['status'] = 1;
$handler->setTranslation($translation);
$handler->saveTranslations();
$info = $this->getHookInfo();
$this->assertTrue(!empty($info['update']), t('Update hook has been properly fired.'));
// Create a Spanish translation and update it before saving it.
$translation = array('source' => 'it', 'language' => 'es');
$handler->setTranslation($translation);
$translation['status'] = 1;
$handler->setTranslation($translation);
$handler->saveTranslations();
$node = node_load($node->nid, NULL, TRUE);
$handler = entity_translation_get_handler('node', $node, TRUE);
$translations = $handler->getTranslations();
$this->assertTrue(!empty($translations->data['es']), t('A Spanish translation has been created'));
$info = $this->getHookInfo();
$this->assertTrue(!empty($info['insert']), t('Insert hook has been properly fired.'));
// Delete a translation after updating it without saving.
$translation['status'] = 0;
$handler->setTranslation($translation);
$handler->removeTranslation('es');
$handler->saveTranslations();
$info = $this->getHookInfo();
$this->assertTrue(empty($info['update']), t('Update hook has not been fired.'));
$info = $this->getHookInfo('delete');
$this->assertTrue(!empty($info['es']), t('Delete hook has been properly fired.'));
}
/**
* Retrieve the information stored by hook implementations.
*/
protected function getHookInfo($op = 'save') {
$name = 'entity_translation_test_' . $op;
$info = variable_get($name);
variable_del($name);
return $info;
}
}

View File

@@ -0,0 +1,14 @@
name = Entity Translation testing
description = Tests Entity Translation module functionality. Do not enable.
core = 7.x
package = Testing
hidden = TRUE
dependencies[] = entity_translation
files[] = entity_translation_test.module
; Information added by drupal.org packaging script on 2013-07-23
version = "7.x-1.0-beta3"
core = "7.x"
project = "entity_translation"
datestamp = "1374601567"

View File

@@ -0,0 +1,7 @@
<?php
/**
* @file
* Installation functionality for Entity Translation testing module.
*/

View File

@@ -0,0 +1,69 @@
<?php
/**
* @file
* Testing functionality for Entity Translation module.
*/
/**
* Implements hook_entity_translation_insert().
*/
function entity_translation_test_entity_translation_insert($entity_type, $entity, $translation, $values = array()) {
entity_translation_test_check_save($entity_type, $entity, $translation, 'insert');
}
/**
* Implements hook_entity_translation_insert().
*/
function entity_translation_test_entity_translation_update($entity_type, $entity, $translation, $values = array()) {
entity_translation_test_check_save($entity_type, $entity, $translation, 'update');
}
/**
* Test implementation for save hooks.
*/
function entity_translation_test_check_save($entity_type, $entity, $translation, $hook) {
list($id, ,) = entity_extract_ids($entity_type, $entity);
$match = FALSE;
$row = db_select('entity_translation', 'et')
->fields('et')
->condition('entity_type', $entity_type)
->condition('entity_id', $id)
->condition('language', $translation['language'])
->orderBy('created')
->execute()
->fetch(PDO::FETCH_ASSOC);
if ($row) {
$match = TRUE;
foreach ($translation as $key => $value) {
if ($row[$key] != $value) {
$match = FALSE;
break;
}
}
}
variable_set('entity_translation_test_save', array($hook => $match));
}
/**
* Implements hook_entity_translation_delete().
*/
function entity_translation_test_entity_translation_delete($entity_type, $entity, $langcode) {
list($id, ,) = entity_extract_ids($entity_type, $entity);
$row = db_select('entity_translation', 'et')
->fields('et')
->condition('entity_type', $entity_type)
->condition('entity_id', $id)
->condition('language', $langcode)
->orderBy('created')
->execute()
->fetch(PDO::FETCH_ASSOC);
$info = variable_get('entity_translation_test_delete', array());
$info[$langcode] = empty($row);
variable_set('entity_translation_test_delete', $info);
}

View File

@@ -0,0 +1,227 @@
<?php
/**
* @file
*
* Provide views data and handlers for entity_translation.
*/
/**
* Implements hook_views_data().
*/
function entity_translation_views_data() {
$data = array();
$data['entity_translation']['table']['group'] = t('Entity translation');
// Advertise this table as a possible base table.
$data['entity_translation']['table']['base'] = array(
'field' => 'entity_id',
'title' => t('Entity translation'),
'help' => t('Information about a translation of an entity.'),
);
$data['entity_translation']['entity_id'] = array(
'title' => t('Entity id'),
'help' => t('The entity id.'),
'field' => array(
'handler' => 'views_handler_field_numeric',
'click sortable' => TRUE,
),
'argument' => array(
'handler' => 'views_handler_argument_numeric',
'numeric' => TRUE,
'validate type' => 'entity_id',
),
'filter' => array(
'handler' => 'views_handler_filter_numeric',
),
'sort' => array(
'handler' => 'views_handler_sort',
),
);
$data['entity_translation']['entity_type'] = array(
'title' => t('Entity type'),
'help' => t('The entity type.'),
'field' => array(
'handler' => 'views_handler_field',
'click sortable' => TRUE,
),
'argument' => array(
'handler' => 'views_handler_argument_string',
'numeric' => FALSE,
'validate type' => 'entity_type',
),
'filter' => array(
'handler' => 'entity_translation_handler_filter_entity_type',
),
'sort' => array(
'handler' => 'views_handler_sort',
),
);
$data['entity_translation']['language'] = array(
'title' => t('Language'),
'help' => t('The language of this translation.'),
'field' => array(
'handler' => 'views_handler_field_locale_language',
'click sortable' => TRUE,
),
'argument' => array(
'handler' => 'views_handler_argument_locale_language',
'numeric' => FALSE,
'validate type' => 'language',
),
'filter' => array(
'handler' => 'entity_translation_handler_filter_language',
'allow empty' => TRUE,
),
'sort' => array(
'handler' => 'views_handler_sort',
),
);
$data['entity_translation']['source'] = array(
'title' => t('Source'),
'help' => t('The source language.'),
'field' => array(
'handler' => 'views_handler_field_locale_language',
'click sortable' => TRUE,
),
'argument' => array(
'handler' => 'views_handler_argument_locale_language',
'name field' => 'title',
'numeric' => FALSE,
'validate type' => 'string',
),
'filter' => array(
'handler' => 'entity_translation_handler_filter_language',
'allow empty' => TRUE,
),
'sort' => array(
'handler' => 'views_handler_sort',
),
);
$data['entity_translation']['status'] = array(
'title' => t('Translation status'),
'help' => t('The status of this translation.'),
'field' => array(
'handler' => 'views_handler_field_boolean',
'click sortable' => TRUE,
),
'argument' => array(
'handler' => 'views_handler_argument_numeric',
'numeric' => TRUE,
'validate type' => 'boolean',
),
'filter' => array(
'handler' => 'views_handler_filter_boolean_operator',
),
'sort' => array(
'handler' => 'views_handler_sort',
),
);
$data['entity_translation']['translate'] = array(
'title' => t('Needs update'),
'help' => t('Indicates if the translation needs to be updated.'),
'field' => array(
'handler' => 'views_handler_field_boolean',
'click sortable' => TRUE,
),
'argument' => array(
'handler' => 'views_handler_argument_numeric',
'name field' => 'title',
'numeric' => TRUE,
'validate type' => 'boolean',
),
'filter' => array(
'handler' => 'views_handler_filter_boolean_operator',
),
'sort' => array(
'handler' => 'views_handler_sort',
),
);
$data['entity_translation']['created'] = array(
'title' => t('Created'),
'help' => t('Created date.'),
'field' => array(
'handler' => 'views_handler_field_date',
'click sortable' => TRUE,
),
'argument' => array(
'handler' => 'views_handler_argument_date',
'numeric' => FALSE,
'validate type' => 'date',
),
'filter' => array(
'handler' => 'views_handler_filter_date',
),
'sort' => array(
'handler' => 'views_handler_sort_date',
),
);
$data['entity_translation']['changed'] = array(
'title' => t('Changed'),
'help' => t('Changed date.'),
'field' => array(
'handler' => 'views_handler_field_date',
'click sortable' => TRUE,
),
'argument' => array(
'handler' => 'views_handler_argument_date',
'numeric' => FALSE,
'validate type' => 'date',
),
'filter' => array(
'handler' => 'views_handler_filter_date',
),
'sort' => array(
'handler' => 'views_handler_sort_date',
),
);
$data['entity_translation']['translate_link'] = array(
'title' => t('Translate link'),
'help' => t('Link to translation overview page.'),
'field' => array(
'handler' => 'entity_translation_handler_field_translate_link',
),
);
$data['entity_translation']['translation_exists'] = array(
'title' => t('Translation exists'),
'help' => t('Link to translation overview page.'),
'filter' => array(
'handler' => 'entity_translation_handler_filter_translation_exists',
),
);
$data['entity_translation']['label'] = array(
'title' => t('Label'),
'help' => t('The label of the entity.'),
'field' => array(
'handler' => 'entity_translation_handler_field_label',
),
);
return $data;
}
/**
* Implements hook_views_data_alter().
*
* Add entity translation information to the various entity tables.
*/
function entity_translation_views_data_alter(&$data) {
foreach (entity_get_info() as $type => $info) {
if ($info['fieldable'] && isset($data[$info['base table']])) {
$table = &$data[$info['base table']];
$table['entity_translations'] = array(
'title' => t('Entity translation: translations'),
'help' => t('Translation information.'),
'relationship' => array(
'label' => t('Translations'),
'base' => 'entity_translation',
'base field' => 'entity_id',
'relationship field' => $info['entity keys']['id'],
'handler' => 'entity_translation_handler_relationship',
// We add our information here in the definition, so we can copy it
// later.
'left_table' => $info['base table'],
'left_field' => $info['entity keys']['id'],
'entity type' => $type,
),
);
}
}
}

View File

@@ -0,0 +1,78 @@
<?php
/**
* @file
* This file contains a label field handler for entity translation.
*/
/**
* This handler shows the entity label for entities in the entity_translation table.
*/
class entity_translation_handler_field_label extends views_handler_field {
function construct() {
parent::construct();
$this->additional_fields['entity_id'] = 'entity_id';
$this->additional_fields['entity_type'] = 'entity_type';
}
function query() {
$this->ensure_my_table();
$this->add_additional_fields();
}
/**
* Add a 'link to entity' option definition.
* @see views_handler_field::option:definition()
*/
function option_definition() {
$options = parent::option_definition();
$options['link_to_entity'] = array('default' => '', 'translatable' => FALSE);
return $options;
}
/**
* Add a 'link to entity' option.
* @see views_handler_field::options_form()
*/
function options_form(&$form, &$form_state) {
parent::options_form($form, $form_state);
$form['link_to_entity'] = array(
'#title' => t('Link this field to it\'s entity'),
'#type' => 'checkbox',
'#default_value' => $this->options['link_to_entity']
);
}
/**
* Load all entities, so that we can get the label.
*/
function post_execute(&$values) {
$ids = array();
$ids_by_type = array();
foreach ($values as $row) {
if ($entity_type = $this->get_value($row, 'entity_type')) {
$ids_by_type[$entity_type][] = $this->get_value($row, 'entity_id');
}
}
foreach ($ids_by_type as $type => $ids) {
$this->entities[$type] = entity_load($type, $ids);
}
}
function render($values) {
$entity_type = $this->get_value($values, 'entity_type');
$entity_id = $this->get_value($values, 'entity_id');
// Check if entity is not empty
if (!$entity_id || !$entity_type) {
return NULL;
}
$entity = $this->entities[$entity_type][$entity_id];
// We could also use entity_label(), but since this we might want to let
// the handler decide what's best to show.
$handler = entity_translation_get_handler($entity_type, $entity);
$label = $handler->getLabel();
if ($this->options['link_to_entity']) {
$this->options['alter']['make_link'] = TRUE;
$this->options['alter']['path'] = $handler->getViewPath();
}
return $label;
}
}

View File

@@ -0,0 +1,103 @@
<?php
/**
* @file
* Translate link plugin.
*/
/**
* This handler adds translate link for all translatable entities.
*/
class entity_translation_handler_field_translate_link extends views_handler_field {
function construct() {
parent::construct();
$this->additional_fields['entity_id'] = 'entity_id';
$this->additional_fields['entity_type'] = 'entity_type';
$this->additional_fields['language'] = 'language';
}
/**
* Add required additional fields.
*/
function query() {
$this->ensure_my_table();
$this->add_additional_fields();
}
/**
* Add the text option.
* @see views_handler_field::option_definition()
*/
function option_definition() {
$options = parent::option_definition();
$options['text'] = array('default' => '', 'translatable' => TRUE);
return $options;
}
/**
* Add the option to set the title of the translate link.
* @see views_handler_field::options_form()
*/
function options_form(&$form, &$form_state) {
$form['text'] = array(
'#type' => 'textfield',
'#title' => t('Text to display'),
'#default_value' => $this->options['text'],
);
parent::options_form($form, $form_state);
// The path is set by render_link function so don't allow setting it.
$form['alter']['path'] = array('#access' => FALSE);
$form['alter']['external'] = array('#access' => FALSE);
}
/**
* Load all entities based on the data we have.
*/
function post_execute(&$values) {
$ids = array();
$ids_by_type = array();
foreach ($values as $row) {
if ($entity_type = $this->get_value($row, 'entity_type')) {
$ids_by_type[$entity_type][] = $this->get_value($row, 'entity_id');
}
}
foreach ($ids_by_type as $type => $ids) {
$this->entities[$type] = entity_load($type, $ids);
}
}
/**
* @see views_handler_field::render()
*/
function render($values) {
$type = $this->get_value($values, 'entity_type');
$entity_id = $this->get_value($values, 'entity_id');
// Check if entity is not empty
if (!$entity_id || !$type) {
return NULL;
}
$language = $this->get_value($values, 'language');
$entity = $this->entities[$type][$entity_id];
return $this->render_link($type, $entity_id, $entity, $language);
}
/**
* Render the link to the translation overview page of the entity.
*/
function render_link($entity_type, $entity_id, $entity, $language) {
if (!entity_translation_tab_access($entity_type, $entity)) {
return;
}
// We use the entity info here to avoid having to call entity_load() for all
// the entities.
$info = entity_get_info($entity_type);
$path = $info['translation']['entity_translation']['path schemes']['default']['translate path'];
$path = str_replace($info['translation']['entity_translation']['path schemes']['default']['path wildcard'], $entity_id, $path);
$this->options['alter']['make_link'] = TRUE;
$this->options['alter']['path'] = $path;
$this->options['alter']['query'] = drupal_get_destination();
$text = !empty($this->options['text']) ? $this->options['text'] : t('translate');
return $text;
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* @file
* Contains an entity type filter handler.
*/
/**
* This handler shows all available entity types that are enabled for entity
* translation as options.
*/
class entity_translation_handler_filter_entity_type extends views_handler_filter_in_operator {
/**
* Show all entity types that are enabled for entity translation as options.
*/
function get_value_options() {
if (!isset($this->value_options)) {
$this->value_title = t('Entity type');
$allowed_types_options = variable_get('entity_translation_entity_types');
$allowed_types = array();
$entity_info = entity_get_info();
foreach ($allowed_types_options as $key => $allowed) {
if ($allowed) {
$allowed_types[$key] = $entity_info[$key]['label'];
}
}
$this->value_options = $allowed_types;
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* @file
* Contains a language filter handler.
*/
/**
* Extends the locale language filter in order for it to work with the entity
* translation table structure.
*/
class entity_translation_handler_filter_language extends views_handler_filter_locale_language {
/**
* Override the default behaviour, insert an empty string instead of NULL.
*/
function op_empty() {
$this->ensure_my_table();
if ($this->operator == 'empty') {
$operator = "=";
}
else {
$operator = "<>";
}
$this->query->add_where($this->options['group'], "$this->table_alias.$this->real_field", '', $operator);
}
}

View File

@@ -0,0 +1,132 @@
<?php
/**
* @file
* Contains an entity type filter handler.
*/
/**
* This handler determines if a translation exists for a particular translation.
*/
class entity_translation_handler_filter_translation_exists extends views_handler_filter_locale_language {
/**
* Add a 'entity_type' option definition.
* @see views_handler_field::option:definition()
*/
function option_definition() {
$options = parent::option_definition();
$options['entity_type'] = array('default' => '', 'translatable' => FALSE);
$options['use_filter'] = array('default' => '', 'translatable' => FALSE);
$options['filter'] = array('default' => '', 'translatable' => FALSE);
return $options;
}
/**
* Override the default title for the operators.
*/
function operators() {
$operators = parent::operators();
$operators['in']['title'] = t('Translation exists');
$operators['not in']['title'] = t('Translation doesn\'t exist');
return $operators;
}
/**
* Add option for setting entity type either directly or through a filter.
* @see views_handler_field::options_form()
*/
function options_form(&$form, &$form_state) {
parent::options_form($form, $form_state);
$filters = $this->get_entity_type_filters();
if (!empty($filters)) {
$form['use_filter'] = array(
'#type' => 'checkbox',
'#title' => t('Use an entity type filter.'),
'#default_value' => $this->options['use_filter'],
);
$form['filter'] = array(
'#type' => 'select',
'#title' => t('Filter'),
'#options' => $filters,
'#dependency' => array(
'edit-options-use-filter' => array(1)
),
'#default_value' => $this->options['filter'],
);
}
$form['entity_type'] = array(
'#title' => t('Entity type'),
'#type' => 'select',
'#options' => $this->get_allowed_types(),
'#dependency' => array(
'edit-options-use-filter' => array(0)
),
'#default_value' => $this->options['entity_type'],
'#description' => t('You have to filter on a particular entity type when you use this filter'),
);
}
/**
* Get all available entity type filters that can be used to build the query.
*/
function get_entity_type_filters() {
// We need to build the query to know about the available fields.
$this->view->build();
$filters = array();
foreach ($this->view->filter as $key => $filter) {
// Break if we encounter our filter, the filter must be before this one.
if ($filter == $this) {
break;
}
if ($filter instanceof entity_translation_handler_filter_entity_type && count($filter->value) == 1 && empty($filter->options['expose']['multiple'])) {
$filters[$key] = $filter->value_title;
}
}
return $filters;
}
/**
* Get entity types managed by entity translation.
*/
function get_allowed_types() {
$allowed_types_options = variable_get('entity_translation_entity_types');
$allowed_types = array();
$entity_info = entity_get_info();
foreach ($allowed_types_options as $key => $allowed) {
if ($allowed) {
$allowed_types[$key] = $entity_info[$key]['label'];
}
}
return $allowed_types;
}
/**
* Override the default behaviour of the handler.
*/
function query() {
$this->ensure_my_table();
// We need a subquery to determine not in.
if ($this->operator == 'not in') {
$entity_type = 'node';
if ($this->options['use_filter'] && isset($this->view->filter[$this->options['filter']])) {
$filter = $this->view->filter[$this->options['filter']];
$entity_type = current($filter->value);
}
else {
$this->query->add_where($this->options['group'], "$this->table_alias.entity_type", $this->options['entity_type'], '=');
$entity_type = $this->options['entity_type'];
}
$query = db_select('entity_translation', 'es')
->condition('entity_type', $entity_type)
->condition('language', $this->value);
$query->addField('es', 'entity_id');
$this->query->add_where($this->options['group'], "$this->table_alias.entity_id", $query, $this->operator);
}
// We can determine if a translation exists without a subquery.
else {
$value = array_keys($this->value);
$this->query->add_where($this->options['group'], "$this->table_alias.source", '', '<>');
$this->query->add_where($this->options['group'], "$this->table_alias.language", array_values($this->value), $this->operator);
}
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* @file
* Contains the relationship plugin for relating entities to translation metadata.
*/
/**
* Add a relationship to the entity translation table.
*/
class entity_translation_handler_relationship extends views_handler_relationship {
/**
* Add a relationship to the entity_translation table.
*/
function query() {
$this->ensure_my_table();
$def = $this->definition;
$def['table'] = 'entity_translation';
$def['field'] = 'entity_id';
$def['left_table'] = $this->table_alias;
$def['type'] = empty($this->options['required']) ? 'LEFT' : 'INNER';
$join = new views_join();
$join->definition = $def;
$join->construct();
$join->adjusted = TRUE;
// Use a short alias for the table.
$alias = $def['table'] . '_' . $this->table;
// We need to add a condition on entity type to the join to avoid getting
// relationships to entities with other types.
$join->extra = "$alias.entity_type = '{$def['entity type']}'";
$this->alias = $this->query->add_relationship($alias, $join, 'entity_translation', $this->relationship);
}
}