updated core to 7.58 (right after the site was hacked)

This commit is contained in:
2018-04-20 23:48:40 +02:00
parent 18f4aba146
commit 9344a61b61
711 changed files with 99690 additions and 480 deletions

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,34 @@
-- SUMMARY --
Creates tabs on nodes to redirect users to the appropriate transltion edit
page.
For a full description of the module, visit the project page:
http://drupal.org/project/entity_translation_tabs
To submit bug reports and feature suggestions, or to track changes:
http://drupal.org/project/issues/entity_translation_tabs
-- REQUIREMENTS --
entity_translation, and entity translation enabled on your nodes.
-- INSTALLATION --
* Install as usual, see http://drupal.org/node/70151 for further information.
-- CONFIGURATION --
* Then configure your nodes to use entity_translation
-- USAGE --
* Go to a node page, where the edit tab was you will now see an edit tab
for each language in addition to a "Source" tab (which is the renamed
'Edit' tab).
-- CONTACT --
Current maintainers:
* Ryan Weal (Ryan Weal) - http://drupal.org/user/412402

View File

@@ -0,0 +1,14 @@
name = Entity translation tabs
description = Creates a translation tab for each language on nodes.
core = 7.x
package = Multilingual
dependencies[] = locale
dependencies[] = entity_translation
files[] = entity_translation_tabs.module
; Information added by Drupal.org packaging script on 2016-04-09
version = "7.x-1.1"
core = "7.x"
project = "entity_translation_tabs"
datestamp = "1460204042"

View File

@@ -0,0 +1,82 @@
<?php
/**
* @file entity_translation_tabs.module
* This module gives you a tab for each language on entity translation nodes.
*/
/**
* Implements hook_menu_alter().
*/
function entity_translation_tabs_menu_alter(&$items) {
$items['node/%node/edit']['weight'] = 3;
$items['node/%node/edit']['title'] = t('Source');
// It would be nice to hide the translate tab when it is no longer needed
// but this causes problems with the entity_translation module.
}
/**
* Implements hook_menu().
*/
function entity_translation_tabs_menu() {
$languages = language_list('enabled');
// Eventually it would be nice to replace this series of menu entries with
// the same code as the entity_translation module's hook_menu code to allow
// us to show the tabs in taxonomy, user, and other types of entities.
foreach ($languages[1] as $key => $value) {
$items['node/%node/' . "$key"] = array(
'title' => 'Edit' . ' [' . $value->name . ']',
'page callback' => 'entity_translation_tabs_switcher',
'page arguments' => array(1, 2),
'access callback' => 'node_access',
'access arguments' => array('update', 1),
'weight' => 0,
'type' => MENU_LOCAL_TASK,
'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
);
}
return $items;
}
/**
* Redirection logic to determine where the tabs will take you.
*/
function entity_translation_tabs_switcher($node, $lang) {
// This switcher currently does a drupal_goto for each appropriate case, but
// the intended functionality is to load the entity translation form directly
// on each applicable tab. It will require loading the appropraite inc files
// and then to load the entity_translation_edit_form with all the parameters.
$nid = $node->nid;
$und = FALSE;
$result = db_query(
"SELECT language, source FROM {entity_translation} WHERE entity_id=:eid",
array(
':eid' => $nid,
));
foreach ($result as $record) {
// Source is not set, therefore it is source.
if (sizeof($record->source) == 1) {
$source = $record->language;
}
// Source is undefined, therefore edit original.
if ($record->language == 'und') {
$und = TRUE;
}
// Translation exists, let's go there.
if ($record->language == $lang) {
drupal_goto('node/' . $nid . '/edit/' . $lang);
}
}
if ($und != TRUE) {
drupal_set_message(t('This is a new translation, please translate it now.'), 'warning');
drupal_goto('node/' . $nid . '/edit/add/' . $source . "/" . $lang);
}
else {
drupal_set_message(t('This content is set to display on all languages.
Set the language of this page to make it translatable.'), 'warning');
drupal_goto('node/' . $nid . '/edit');
}
return;
}

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,288 @@
diff --git a/i18n_access.module b/i18n_access.module
index 5f4aa56..a76370d 100644
--- a/i18n_access.module
+++ b/i18n_access.module
@@ -105,18 +105,21 @@ function i18n_access_permission() {
function i18n_access_form_node_form_alter(&$form, &$form_state, $form_id) {
if (isset($form['language']['#options'])) {
- // Remove inaccessible languages from the select box
- // don't do it for admininstrators
+ // Remove inaccessible languages from the select box
+ // don't do it for administrators
if (!user_access('administer nodes')) {
$perms = i18n_access_load_permissions();
foreach ($form['language']['#options'] as $key => $value) {
$perm_key = ($key == '') ? I18N_ACCESS_LANGUAGE_NEUTRAL : $key;
- if ($key!='en' && empty($perms[$perm_key])) {
+
+ // remove english from here, we treat english the same as any language
+ // if ($key!='en' && empty($perms[$perm_key])) {
+ if (empty($perms[$perm_key])) {
unset($form['language']['#options']["$key"]);
}
}
}
- unset($form['#after_build']['0']);
+ // unset($form['#after_build']['0']);
}
}
@@ -125,7 +128,7 @@ function i18n_access_form_node_form_alter(&$form, &$form_state, $form_id) {
*/
function i18n_access_form_alter(&$form, &$form_state, $form_id) {
- //Configuring translation edit form to limit it to allowed language
+ // Configuring translation edit form to limit it to allowed language
if ($form_id == 'i18n_node_select_translation' && !user_access('administer nodes')) {
$perms = i18n_access_load_permissions();
@@ -149,7 +152,7 @@ function i18n_access_form_alter(&$form, &$form_state, $form_id) {
}
// Add i18n_access things to user/edit /user/add
- if ($form_id == 'user_register_form' || $form_id == 'user_profile_form' ) {
+ if ($form_id == 'user_register_form' || $form_id == 'user_profile_form') {
$form['i18n_access'] = array(
'#type' => 'fieldset',
@@ -171,67 +174,100 @@ function i18n_access_form_alter(&$form, &$form_state, $form_id) {
*
* @see node_access()
*/
-function i18n_access_node_access($node, $op, $account = NULL) {
+function i18n_access_node_access($node, $op, $account = NULL, $langcode = NULL) {
+ // big re-work here. discarded entire original function-- replaced with our own
if (is_object($node)) {
-
- global $user;
-
- // If no user object is supplied, the access check is for the current user.
- if (empty($account)) {
- $account = $user;
+ // make sure that site administrators always have access
+ $permissions = i18n_access_load_permissions($user);
+ if (user_access('site administrator', $account)) {
+ return TRUE;
}
+ // if langcode is null it means the user is not accessing by translation overview, we throw access deny and allow to hard deny sneaky people and keep unpermitted tabs out of the menu system for the user
+ elseif ($langcode == NULL) {
+ global $language;
+ $langcode = $language->language;
+
+ switch ($op) {
+ case 'view':
+ return NODE_ACCESS_ALLOW;
+ break;
+ case 'update':
+ if (empty($permissions[$langcode])) {
+ return NODE_ACCESS_DENY;
+ }
+ else {
+ return NODE_ACCESS_ALLOW;
+ }
+ break;
+ case 'create':
+ if (empty($permissions[$langcode])) {
+ return NODE_ACCESS_DENY;
+ }
+ else {
+ return NODE_ACCESS_ALLOW;
+ }
+ break;
- // Bypass completely if node_access returns false.
- //TODO $access = node_access($node, $op, $account);
-
- /* TODO if (!$access) {
- return FALSE;
- } */
-
- // This module doesn't deal with view permissions
- if ($op == 'view') {
- return NODE_ACCESS_IGNORE;
+ }
}
-
- // make sure that administrators always have access
- if (user_access('administer nodes', $account)) {
- return TRUE;
+ //if they are accessing by translation overview, the language code gets passed by the translation overview, we send true or false here
+ else {
+ switch ($op) {
+ case 'view':
+ return TRUE;
+ break;
+ case 'update':
+ if (empty($permissions[$langcode])) {
+ return FALSE;
+ }
+ else {
+ return TRUE;
+ }
+ break;
+ case 'create':
+ if (empty($permissions[$langcode])) {
+ return FALSE;
+ }
+ else {
+ return TRUE;
+ }
+ break;
+ }
}
-
- $perms = i18n_access_load_permissions($account->uid);
-
- // Make sure to use the language neutral constant if node language is empty
- $langcode = $node->language ? $node->language : I18N_ACCESS_LANGUAGE_NEUTRAL;
-
- //return isset($perms[$langcode]) ? (bool) $perms[$langcode] : NODE_ACCESS_DENY;
- return isset($perms[$langcode]) ? NODE_ACCESS_ALLOW : NODE_ACCESS_DENY;
}
}
/**
* Implements hook_menu_alter().
*/
-function i18n_access_menu_alter(&$items) {
+
+//make function name i18n_access_node_menu_alter
+function i18n_access_node_menu_alter(&$items) {
+
+ // due to hook_module_implementation_alter calling entity translation last, we can't change the callback here, i've done it in entity_translation.node.inc - consider calling it here?
// Replace the translation overview page since we can't hook it.
$items['node/%node/translate']['page callback'] = 'i18n_access_translation_node_overview';
+
}
function i18n_access_translation_node_overview($node) {
include_once DRUPAL_ROOT . '/includes/language.inc';
- if (!empty($node->tnid)) {
- // Already part of a set, grab that set.
- $tnid = $node->tnid;
- $translations = translation_node_get_translations($node->tnid);
- }
- else {
- // We have no translation source nid, this could be a new set, emulate that.
- $tnid = $node->nid;
- $translations = array($node->language => $node);
+ // include functions from i18n_node.pages.inc
+ include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'i18n_node') . '/i18n_node.pages.inc';
+
+ // this is the part where this thing sorts out how to build a list of existing translations for this node
+ // since we use entity translation, the tnid isn't what we're using to build the translation list. we're using node->translations->data[keys]
+ $available_translations = $node->translations->data;
+
+ // iterate over each available translation and add its key (which is the 2 letter language code) to the array we call $translations with the node object as the value
+ foreach ($available_translations as $key => $value) {
+ $translations[$key] = $node;
}
$type = variable_get('translation_language_type', LANGUAGE_TYPE_INTERFACE);
+
$header = array(t('Language'), t('Title'), t('Status'), t('Operations'));
//added from i18n/i18n_node/i18n_node.pages.inc function
@@ -240,9 +276,9 @@ function i18n_access_translation_node_overview($node) {
$perms = i18n_access_load_permissions($account->uid);
//end
-
// Modes have different allowed languages
foreach (i18n_node_language_list($node) as $langcode => $language_name) {
+
if ($langcode == LANGUAGE_NONE) {
// Never show language neutral on the overview.
continue;
@@ -253,16 +289,24 @@ function i18n_access_translation_node_overview($node) {
// We load the full node to check whether the user can edit it.
$translation_node = node_load($translations[$langcode]->nid);
$path = 'node/' . $translation_node->nid;
- $title = i18n_node_translation_link($translation_node->title, $path, $langcode);
- if (node_access('update', $translation_node)) {
+
+ // Account for title field module:
+ if (isset($translation_node->title_field) && isset($translation_node->title_field[$langcode])) {
+ $title = i18n_node_translation_link($translation_node->title_field[$langcode][0]['value'], $path, $langcode);
+ }
+ else {
+ $title = i18n_node_translation_link($translation_node->title, $path, $langcode);
+ }
+
+ if (i18n_access_node_access($translation_node, 'update', $user, $langcode)) {
$text = t('edit');
$path = 'node/' . $translation_node->nid . '/edit';
$options[] = i18n_node_translation_link($text, $path, $langcode);
}
$status = $translation_node->status ? t('Published') : t('Not published');
- $status .= $translation_node->translate ? ' - <span class="marker">' . t('outdated') . '</span>' : '';
+ $status .= $translation_node->translate ? ' - ' . t('outdated') . '' : '';
if ($translation_node->nid == $tnid) {
- $language_name = t('<strong>@language_name</strong> (source)', array('@language_name' => $language_name));
+ $language_name = t('@language_name (source)', array('@language_name' => $language_name));
}
}
else {
@@ -316,6 +360,52 @@ function i18n_access_menu() {
}
/**
+ * Node-specific menu alterations.
+ */
+function i18n_access_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 {
+ $access_arguments = array(1);
+ }
+
+ // Point the 'translate' tab to point to the i18n_access version of the translation overview page
+ $items['node/%node/translate']['page callback'] = 'i18n_access_translation_node_overview';
+
+ // There are 3 page arguments for the entity translation overview, only one for i18n_access:
+ $items['node/%node/translate']['page arguments'] = array(1);
+
+ // Pass in the i18n_access permissions
+ $items['node/%node/translate']['access arguments'] = $access_arguments;
+
+ // Point to i18n_access's include for the callback
+ $items['node/%node/translate']['file'] = 'i18n_access.module';
+
+ // Point to i18n_access module
+ $items['node/%node/translate']['module'] = 'i18n_access';
+}
+
+/**
+ * Implements hook_module_implements_alter().
+ */
+function i18n_access_module_implements_alter(&$implementations, $hook) {
+ switch ($hook) {
+ case 'menu_alter':
+ // Move our hook_menu_alter implementation to the end of the list.
+ $group = $implementations['i18n_access'];
+ unset($implementations['i18n_access']);
+ $implementations['i18n_access'] = $group;
+ break;
+ }
+}
+
+/**
* Admin settings form
*/
function i18n_access_admin_settings() {
@@ -330,4 +420,4 @@ function i18n_access_admin_settings() {
);
return system_settings_form($form);
-}
\ No newline at end of file
+}

View File

@@ -0,0 +1,619 @@
diff --git a/i18n_access.info b/i18n_access.info
index e19050a..074cec2 100644
--- a/i18n_access.info
+++ b/i18n_access.info
@@ -2,6 +2,9 @@ name = Translation Access
description = Control access to creating content in different languages.
package = Multilanguage
core = 7.x
+configure = admin/config/regional/language/access
dependencies[] = locale
dependencies[] = translation
+dependencies[] = i18n_node
+files[] = i18n_access.test
diff --git a/i18n_access.install b/i18n_access.install
index 24aefb4..95c935c 100644
--- a/i18n_access.install
+++ b/i18n_access.install
@@ -42,4 +42,5 @@ function i18n_access_install() {
* Implements hook_uninstall().
*/
function i18n_access_uninstall() {
+ variable_del('i18n_access_languages');
}
diff --git a/i18n_access.module b/i18n_access.module
index 5f4aa56..3b68458 100644
--- a/i18n_access.module
+++ b/i18n_access.module
@@ -2,32 +2,14 @@
/**
* @file
- * file_description
+ * i18n_access.module
*/
-define('I18N_ACCESS_LANGUAGE_NEUTRAL', 'NEUTRAL');
-
/**
* Implements hook_user_insert().
*/
function i18n_access_user_insert(&$edit, &$account, $category = NULL) {
- if ($category == 'account') {
- // see user_admin_perm_submit()
- if (isset($edit['i18n_access'])) {
- db_delete('i18n_access')
- ->condition('uid', $account->uid)
- ->execute();
- $edit['i18n_access'] = array_filter($edit['i18n_access']);
- if (count($edit['i18n_access'])) {
- db_insert('i18n_access')
- ->fields(array(
- 'uid' => $account->uid,
- 'perm' => implode(', ', array_keys($edit['i18n_access'])),
- ))->execute();
- }
- unset($edit['i18n_access']);
- }
- }
+ i18n_access_user_update($edit, $account, $category);
}
/**
@@ -54,10 +36,19 @@ function i18n_access_user_update(&$edit, &$account, $category = NULL) {
}
/**
+ * Implements hook_user_delete().
+ */
+function i18n_access_user_delete($account) {
+ db_delete('i18n_access')
+ ->condition('uid', $account->uid)
+ ->execute();
+}
+
+/**
* Load the language permissions for a given user
*/
function i18n_access_load_permissions($uid = NULL) {
- static $perms = array();
+ $perms = &drupal_static(__FUNCTION__);
// use the global user id if none is passed
if (!isset($uid)) {
@@ -94,7 +85,8 @@ function i18n_access_permission() {
return array(
'access selected languages' => array(
'title' => t('Access selected languages'),
- 'description' => t('access selected languages.'),
+ 'description' => t('This permission gives this role edit/delete access to all content which are in the <a href="!url" target="_blank">selected language</a>. View/create access needs a different access level.', array('!url' => url('admin/config/regional/language/access'))),
+ 'restrict access' => TRUE,
),
);
}
@@ -102,34 +94,39 @@ function i18n_access_permission() {
/**
* Implements hook_form_node_form_alter().
*/
-function i18n_access_form_node_form_alter(&$form, &$form_state, $form_id) {
-
- if (isset($form['language']['#options'])) {
- // Remove inaccessible languages from the select box
- // don't do it for admininstrators
- if (!user_access('administer nodes')) {
- $perms = i18n_access_load_permissions();
- foreach ($form['language']['#options'] as $key => $value) {
- $perm_key = ($key == '') ? I18N_ACCESS_LANGUAGE_NEUTRAL : $key;
- if ($key!='en' && empty($perms[$perm_key])) {
- unset($form['language']['#options']["$key"]);
- }
+function i18n_access_form_node_form_alter(&$form) {
+ $form['#after_build'][] = '_i18n_access_form_node_form_alter';
+}
+
+/**
+ * Unset's languages from language options if user does not have permission to
+ * use.
+ *
+ * @param $form
+ * @param $form_state
+ * @return mixed
+ */
+function _i18n_access_form_node_form_alter($form, &$form_state) {
+ if (isset($form['language']['#options']) && !user_access('bypass node access')) {
+ $perms = i18n_access_load_permissions();
+ foreach ($form['language']['#options'] as $key => $value) {
+ if (empty($perms[$key])) {
+ unset($form['language']['#options'][$key]);
}
}
- unset($form['#after_build']['0']);
}
+
+ return $form;
}
/**
* Implements hook_form_alter().
*/
function i18n_access_form_alter(&$form, &$form_state, $form_id) {
-
//Configuring translation edit form to limit it to allowed language
- if ($form_id == 'i18n_node_select_translation' && !user_access('administer nodes')) {
+ if ($form_id == 'i18n_node_select_translation' && !user_access('bypass node access')) {
$perms = i18n_access_load_permissions();
-
foreach ($form['translations']['nid'] as $language => $translation) {
if (!isset($perms[$language]) && $language != '#tree') {
unset($form['translations']['nid'][$language]);
@@ -159,17 +156,15 @@ function i18n_access_form_alter(&$form, &$form_state, $form_id) {
);
$form['i18n_access']['i18n_access'] = array(
'#type' => 'checkboxes',
- '#options' => array(I18N_ACCESS_LANGUAGE_NEUTRAL => t('Language neutral')) + locale_language_list('name'),
+ '#options' => array(LANGUAGE_NONE => t('Language neutral')) + locale_language_list('name'),
'#default_value' => i18n_access_load_permissions($form['#user']->uid),
- '#description' => t('Select the languages that this user should have permission to create and edit content for.'),
+ '#description' => t('The user get edit, delete access to all content which are in this enabled languages. Create, view access needs a different access level.'),
);
}
}
/**
- * Wrapper around node_access() with additional checks for language permissions.
- *
- * @see node_access()
+ * Implements hook_node_access().
*/
function i18n_access_node_access($node, $op, $account = NULL) {
if (is_object($node)) {
@@ -181,29 +176,16 @@ function i18n_access_node_access($node, $op, $account = NULL) {
$account = $user;
}
- // Bypass completely if node_access returns false.
- //TODO $access = node_access($node, $op, $account);
-
- /* TODO if (!$access) {
- return FALSE;
- } */
-
// This module doesn't deal with view permissions
if ($op == 'view') {
return NODE_ACCESS_IGNORE;
}
- // make sure that administrators always have access
- if (user_access('administer nodes', $account)) {
- return TRUE;
- }
-
$perms = i18n_access_load_permissions($account->uid);
// Make sure to use the language neutral constant if node language is empty
- $langcode = $node->language ? $node->language : I18N_ACCESS_LANGUAGE_NEUTRAL;
+ $langcode = $node->language ? $node->language : LANGUAGE_NONE;
- //return isset($perms[$langcode]) ? (bool) $perms[$langcode] : NODE_ACCESS_DENY;
return isset($perms[$langcode]) ? NODE_ACCESS_ALLOW : NODE_ACCESS_DENY;
}
}
@@ -212,14 +194,26 @@ function i18n_access_node_access($node, $op, $account = NULL) {
* Implements hook_menu_alter().
*/
function i18n_access_menu_alter(&$items) {
- // Replace the translation overview page since we can't hook it.
- $items['node/%node/translate']['page callback'] = 'i18n_access_translation_node_overview';
+ if (isset($items['node/%node/translate'])) {
+ $items['node/%node/translate']['page callback'] = 'i18n_access_translation_node_overview';
+ }
}
+/**
+ * Most logic comes from translation/i18n_node module.
+ *
+ * We removes here only the "add translation" links for languages which are not your selected language.
+ *
+ * @see translation_node_overview
+ * @see i18n_node_translation_overview
+ *
+ * @param object $node
+ *
+ * @return array.
+ */
function i18n_access_translation_node_overview($node) {
include_once DRUPAL_ROOT . '/includes/language.inc';
-
if (!empty($node->tnid)) {
// Already part of a set, grab that set.
$tnid = $node->tnid;
@@ -231,16 +225,12 @@ function i18n_access_translation_node_overview($node) {
$translations = array($node->language => $node);
}
- $type = variable_get('translation_language_type', LANGUAGE_TYPE_INTERFACE);
$header = array(t('Language'), t('Title'), t('Status'), t('Operations'));
-
- //added from i18n/i18n_node/i18n_node.pages.inc function
+ $rows = array();
global $user;
- $account = $user;
- $perms = i18n_access_load_permissions($account->uid);
+ $perms = i18n_access_load_permissions($user->uid);
//end
-
// Modes have different allowed languages
foreach (i18n_node_language_list($node) as $langcode => $language_name) {
if ($langcode == LANGUAGE_NONE) {
@@ -268,15 +258,11 @@ function i18n_access_translation_node_overview($node) {
else {
// No such translation in the set yet: help user to create it.
$title = t('n/a');
- if (node_access('create', $node)) {
+ if (node_access('create', $node->type) && (!empty($perms[$langcode]) || user_access('bypass node access'))) {
$text = t('add translation');
$path = 'node/add/' . str_replace('_', '-', $node->type);
$query = array('query' => array('translation' => $node->nid, 'target' => $langcode));
-
- //condition added from i18n/i18n_node/i18n_node.pages.inc
- if (in_array($langcode, $perms)) {
- $options[] = i18n_node_translation_link($text, $path, $langcode, $query);
- }
+ $options[] = i18n_node_translation_link($text, $path, $langcode, $query);
}
$status = t('Not translated');
}
@@ -301,9 +287,7 @@ function i18n_access_translation_node_overview($node) {
* Implements hook_menu().
*/
function i18n_access_menu() {
- $items = array();
-
- $items['admin/settings/language/access'] = array(
+ $items['admin/config/regional/language/access'] = array(
'title' => 'Access',
'page callback' => 'drupal_get_form',
'page arguments' => array('i18n_access_admin_settings'),
@@ -311,23 +295,20 @@ function i18n_access_menu() {
'type' => MENU_LOCAL_TASK,
'weight' => 10,
);
-
return $items;
}
/**
- * Admin settings form
+ * Admin settings form.
*/
-function i18n_access_admin_settings() {
-
+function i18n_access_admin_settings($form) {
$form['i18n_access_languages'] = array(
'#title' => t('Select the default access languages'),
'#type' => 'select',
- '#multiple' => 'true',
- '#options' => array(I18N_ACCESS_LANGUAGE_NEUTRAL => t('Language neutral')) + locale_language_list('name'),
+ '#multiple' => TRUE,
+ '#options' => array(LANGUAGE_NONE => t('Language neutral')) + locale_language_list('name'),
'#default_value' => variable_get('i18n_access_languages', array()),
'#description' => t("This selection of languages will be connected with the 'access selected languages' permission which you can use to grant a role access to these languages at once.")
);
-
return system_settings_form($form);
-}
\ No newline at end of file
+}
diff --git a/i18n_access.test b/i18n_access.test
index d32dbf4..5b2f443 100644
--- a/i18n_access.test
+++ b/i18n_access.test
@@ -6,10 +6,17 @@
*/
class i18nAccessTestCase extends DrupalWebTestCase {
+
+ protected $admin_user;
+
+ protected $translator;
+
+ protected $visitor;
+
/**
* Implementation of getInfo().
*/
- function getInfo() {
+ public static function getInfo() {
return array(
'name' => t('Translation Access'),
'description' => t('Test suite for the i18n_access module.'),
@@ -20,22 +27,21 @@ class i18nAccessTestCase extends DrupalWebTestCase {
/**
* Implementation of setUp().
*/
- function setUp() {
- parent::setUp('locale', 'translation', 'i18n_access');
+ public function setUp() {
+ parent::setUp(array('locale', 'translation', 'i18n_access', 'i18n_node'));
- $this->admin_user = $this->drupalCreateUser(array('administer languages', 'administer site configuration', 'access administration pages', 'administer content types', 'administer nodes', 'administer users'));
- $this->translator = $this->drupalCreateUser(array('create story content', 'edit own story content', 'translate content'));
+ $this->admin_user = $this->drupalCreateUser(array('administer languages', 'administer site configuration', 'access administration pages', 'administer content types', 'administer users', 'bypass node access', 'translate content'));
+ $this->translator = $this->drupalCreateUser(array('create article content', 'edit own article content', 'translate content'));
$this->visitor = $this->drupalCreateUser(array('access content'));
$this->drupalLogin($this->admin_user);
+
$this->addLanguage('fr');
$this->addLanguage('de');
- $this->setLanguagePermissions($this->translator, array('en', 'fr'));
// Set Story content type to use multilingual support with translation.
- $edit = array();
$edit['language_content_type'] = 2;
- $this->drupalPost('admin/content/node-type/story', $edit, t('Save content type'));
- $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Story')), t('Story content type has been updated.'));
+ $this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type'));
+ $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Article')), 'Story content type has been updated.');
}
@@ -47,26 +53,27 @@ class i18nAccessTestCase extends DrupalWebTestCase {
*/
function addLanguage($language_code) {
// Check to make sure that language has not already been installed.
- $this->drupalGet('admin/settings/language');
+ $this->drupalGet('admin/config/regional/language');
if (strpos($this->drupalGetContent(), 'enabled[' . $language_code . ']') === FALSE) {
// Doesn't have language installed so add it.
$edit = array();
$edit['langcode'] = $language_code;
- $this->drupalPost('admin/settings/language/add', $edit, t('Add language'));
+ $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
- $languages = language_list('language', TRUE); // Make sure not using cached version.
- $this->assertTrue(array_key_exists($language_code, $languages), t('Language was installed successfully.'));
+ drupal_static_reset('language_list'); // Make sure not using cached version.
+ $languages = language_list('language');
+ $this->assertTrue(array_key_exists($language_code, $languages), 'Language was installed successfully.');
if (array_key_exists($language_code, $languages)) {
- $this->assertRaw(t('The language %language has been created and can now be used.', array('%language' => $languages[$language_code]->name)), t('Language has been created.'));
+ $this->assertRaw(t('The language %language has been created and can now be used.', array('%language' => $languages[$language_code]->name)), 'Language has been created.');
}
}
else {
// Ensure that it is enabled.
$this->drupalPost(NULL, array('enabled[' . $language_code . ']' => TRUE), t('Save configuration'));
- $this->assertRaw(t('Configuration saved.'), t('Language successfully enabled.'));
+ $this->assertRaw(t('Configuration saved.'), 'Language successfully enabled.');
}
}
@@ -80,8 +87,9 @@ class i18nAccessTestCase extends DrupalWebTestCase {
* An array of language codes to give permission for
*/
function setLanguagePermissions($account, $languages = array()) {
- $this->assertTrue(user_access('administer users'), t('User has permission to administer users'));
-
+ $this->assertTrue(user_access('administer users'), 'User has permission to administer users');
+ $expected = array();
+ $edit = array();
foreach ($languages as $langcode) {
$key = 'i18n_access[' . $langcode . ']';
$edit[$key] = $langcode;
@@ -90,7 +98,31 @@ class i18nAccessTestCase extends DrupalWebTestCase {
$this->drupalPost('user/' . $account->uid . '/edit', $edit, t('Save'));
$actual = i18n_access_load_permissions($account->uid);
- $this->assertEqual($expected, $actual, t('Language permissions set correctly.'), 'i18n_access');
+ $this->assertEqual($expected, $actual, 'Language permissions set correctly.', 'i18n_access');
+ }
+
+ /**
+ * Unsets the language permission for the specified user. Must be logged in as
+ * an 'administer users' privileged user before calling this.
+ *
+ * @param $account
+ * The user account to modify
+ * @param $languages
+ * An array of language codes to remove permission for
+ */
+ function unsetLanguagePermissions($account, $languages = array()) {
+ $this->assertTrue(user_access('administer users'), 'User has permission to administer users');
+ $expected = array();
+ $edit = array();
+ foreach ($languages as $langcode) {
+ $key = 'i18n_access[' . $langcode . ']';
+ $edit[$key] = FALSE;
+ }
+ $this->drupalPost('user/' . $account->uid . '/edit', $edit, t('Save'));
+ drupal_static_reset('i18n_access_load_permissions');
+ drupal_static_reset('node_access');
+ $actual = i18n_access_load_permissions($account->uid);
+ $this->assertEqual($expected, $actual, 'Language permissions unset correctly.', 'i18n_access');
}
/**
@@ -109,7 +141,6 @@ class i18nAccessTestCase extends DrupalWebTestCase {
function assertLanguageOption($langcode, $message, $group = 'Other') {
$xpath = '//select[@name="language"]/option';
$fields = $this->xpath($xpath);
-
// If value specified then check array for match.
$found = TRUE;
if (isset($langcode)) {
@@ -157,52 +188,145 @@ class i18nAccessTestCase extends DrupalWebTestCase {
return $this->assertFalse($fields && $found, $message, $group);
}
- function dsm($object) {
- $this->error('<pre>' . check_plain(print_r($object, 1)) . '</pre>');
- }
-
/**
- * Test translator user. User with 'create story content' and 'edit own story
- * content' permissions should be able to create and edit story nodes only in
+ * Test translator user. User with 'create article content' permission
+ * should be able to create and edit article nodes only in/for
* the languages that they have permissions for.
*/
function testTranslatorUser() {
+ $this->_testTranslatorNodeAccess();
+ $this->_testTranslatorNodeAccess(TRUE);
+ }
+
+
+ function _testTranslatorNodeAccess($via_role = FALSE) {
+ $this->drupalLogin($this->admin_user);
+ if (!$via_role) {
+ $this->setLanguagePermissions($this->translator, array('en', 'fr'));
+ }
+ else{
+ $edit = array(
+ 'i18n_access_languages[]' => array('en', 'fr'),
+ );
+ $this->drupalPost('admin/config/regional/language/access', $edit, t('Save configuration'));
+
+ $this->translator = $this->drupalCreateUser(array('create article content', 'edit own article content', 'translate content', 'access selected languages'));
+ }
+
$this->drupalLogin($this->translator);
- $this->drupalGet('node/add/story');
- $this->assertField('language', t('Found language selector.'));
+ $this->drupalGet('node/add/article');
+ $this->assertField('language', 'Found language selector.');
$perms = i18n_access_load_permissions($this->translator->uid);
$languages = language_list();
- $languages[I18N_ACCESS_LANGUAGE_NEUTRAL] = (object)array('language' => '', 'name' => 'Language Neutral');
+ $languages[LANGUAGE_NONE] = (object)array('language' => LANGUAGE_NONE, 'name' => 'Language Neutral');
foreach ($languages as $key => $language) {
// TODO: Add in check for language neutral
if (isset($perms[$key]) && $perms[$key]) {
- $this->assertLanguageOption($language->language, t('Option found for %language in language selector.', array('%language' => $language->name)));
+ $this->assertLanguageOption($language->language, format_string('Option found for %language in language selector.', array('%language' => $language->name)));
}
else {
- $this->assertNoLanguageOption($language->language, t('Option not found for %language in language selector.', array('%language' => $language->name)));
+ $this->assertNoLanguageOption($language->language, format_string('Option not found for %language in language selector.', array('%language' => $language->name)));
}
}
- }
+ $this->drupalLogin($this->admin_user);
+ $node = $this->drupalCreateNode(array('type' => 'article', 'language' => 'de', 'body' => array('de' => array(array()))));
+
+ $this->drupalLogin($this->translator);
+ $this->assertFalse(node_access('update', $node, $this->loggedInUser));
+ $this->drupalGet('node/' . $node->nid . '/edit');
+ $this->assertResponse(403);
+
+ $this->assertFalse(node_access('delete', $node, $this->loggedInUser));
+ $this->drupalGet('node/' . $node->nid . '/delete');
+ $this->assertResponse(403);
+
+ $this->drupalLogin($this->admin_user);
+ $node = $this->drupalCreateNode(array('type' => 'article', 'language' => 'fr', 'body' => array('fr' => array(array()))));
+
+ $this->drupalLogin($this->translator);
+ $this->assertTrue(node_access('update', $node, $this->loggedInUser));
+ $this->drupalGet('node/' . $node->nid . '/edit');
+ $this->assertResponse(200);
+
+ $this->assertTrue(node_access('delete', $node, $this->loggedInUser));
+ $this->drupalGet('node/' . $node->nid . '/delete');
+ $this->assertResponse(200);
+
+ $this->drupalGet('node/' . $node->nid . '/translate');
+ $query = array('query' => array('translation' => $node->nid, 'target' => 'de'));
+ $this->assertNoRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'de', $query));
+ $query = array('query' => array('translation' => $node->nid, 'target' => 'en'));
+ $this->assertRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'en', $query));
+ $this->assertRaw(i18n_node_translation_link(t('edit'), 'node/' . $node->nid . '/edit', 'fr'));
+
+ $this->drupalLogin($this->admin_user);
+ if (!$via_role) {
+ $this->unsetLanguagePermissions($this->translator, array('fr', 'en'));
+ }
+ else{
+ $edit = array(
+ 'i18n_access_languages[]' => array(),
+ );
+ $this->drupalPost('admin/config/regional/language/access', $edit, t('Save configuration'));
+ $this->translator = $this->drupalCreateUser(array('create article content', 'edit own article content', 'translate content', 'access selected languages'));
+ drupal_static_reset('i18n_access_load_permissions');
+ drupal_static_reset('node_access');
+ }
+
+ $this->drupalLogin($this->translator);
+ $this->assertFalse(node_access('update', $node, $this->loggedInUser));
+ $this->drupalGet('node/' . $node->nid . '/edit');
+ $this->assertResponse(403);
+
+ $this->assertFalse(node_access('delete', $node, $this->loggedInUser));
+ $this->drupalGet('node/' . $node->nid . '/delete');
+ $this->assertResponse(403);
+
+ $this->drupalGet('node/' . $node->nid . '/translate');
+ $query = array('query' => array('translation' => $node->nid, 'target' => 'de'));
+ $this->assertNoRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'de', $query));
+ $query = array('query' => array('translation' => $node->nid, 'target' => 'en'));
+ $this->assertNoRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'en', $query));
+ $this->assertNoRaw(i18n_node_translation_link(t('edit'), 'node/' . $node->nid . '/edit', 'fr'));
+
+ }
/**
- * Test admin user. User with 'administer nodes' permission should be able to
- * create and edit nodes regardless of the language
+ * Test admin user. User with 'bypass node access' permission should be able to
+ * update, delete nodes regardless of the language.
*/
function testAdminUser() {
$this->drupalLogin($this->admin_user);
+ $this->drupalGet('node/add/article');
+ $this->assertField('language', 'Found language selector.');
- $this->drupalGet('node/add/story');
- $this->assertField('language', t('Found language selector.'));
-
- $perms = i18n_access_load_permissions($this->admin_user->uid);
$languages = language_list();
- $languages[I18N_ACCESS_LANGUAGE_NEUTRAL] = (object)array('language' => '', 'name' => 'Language Neutral');
+ $languages[LANGUAGE_NONE] = (object)array('language' => LANGUAGE_NONE, 'name' => 'Language Neutral');
foreach ($languages as $language) {
- // TODO: Add in check for language neutral
- $this->assertLanguageOption($language->language, t('Option found for %language, regardless of permission, for administrator.', array('%language' => $language->name)));
+ $this->assertLanguageOption($language->language, format_string('Option found for %language, regardless of permission, for administrator.', array('%language' => $language->name)));
}
+ $this->drupalLogin($this->translator);
+ $node = $this->drupalCreateNode(array('type' => 'article', 'language' => 'de', 'body' => array('de' => array(array()))));
+
+ $this->drupalLogin($this->admin_user);
+
+ $this->assertTrue(node_access('update', $node, $this->loggedInUser));
+ $this->drupalGet('node/' . $node->nid . '/edit');
+ $this->assertResponse(200);
+
+ $this->assertTrue(node_access('delete', $node, $this->loggedInUser));
+ $this->drupalGet('node/' . $node->nid . '/delete');
+ $this->assertResponse(200);
+
+ $this->drupalGet('node/' . $node->nid . '/translate');
+
+ $query = array('query' => array('translation' => $node->nid, 'target' => 'fr'));
+ $this->assertRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'fr', $query));
+ $query = array('query' => array('translation' => $node->nid, 'target' => 'en'));
+ $this->assertRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'en', $query));
+ $this->assertRaw(i18n_node_translation_link(t('edit'), 'node/' . $node->nid . '/edit', 'de'));
}
-}
\ No newline at end of file
+}

View File

@@ -0,0 +1,14 @@
name = Translation Access
description = Control access to creating content in different languages.
package = Multilanguage
core = 7.x
dependencies[] = locale
dependencies[] = translation
; Information added by drupal.org packaging script on 2013-09-30
version = "7.x-1.x-dev"
core = "7.x"
project = "i18n_access"
datestamp = "1380582441"

View File

@@ -0,0 +1,14 @@
name = Translation Access
description = Control access to creating content in different languages.
package = Multilanguage
core = 7.x
dependencies[] = locale
dependencies[] = translation
; Information added by drupal.org packaging script on 2013-09-30
version = "7.x-1.x-dev"
core = "7.x"
project = "i18n_access"
datestamp = "1380582441"

View File

@@ -0,0 +1,12 @@
--- i18n_access.info
+++ i18n_access.info
@@ -2,6 +2,9 @@ name = Translation Access
description = Control access to creating content in different languages.
package = Multilanguage
core = 7.x
+configure = admin/config/regional/language/access
dependencies[] = locale
dependencies[] = translation
+dependencies[] = i18n_node
+files[] = i18n_access.test

View File

@@ -0,0 +1,46 @@
<?php
/**
* @file
* file_description
*/
/**
* Implements hook_schema().
*/
function i18n_access_schema() {
$schema['i18n_access'] = array(
'description' => 'Store language permissions per user',
'fields' => array(
'uid' => array(
'description' => 'The primary identifier for a user.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
'perm' => array(
'description' => 'List of languages that the user has permission for.',
'type' => 'text',
'not null' => FALSE,
'size' => 'big',
),
),
'primary key' => array('uid'),
);
return $schema;
}
/**
* Implements hook_install().
*/
function i18n_access_install() {
// Set module weight for it to run after core and i18n modules
db_query("UPDATE {system} SET weight = 20 WHERE name = 'i18n_access' AND type = 'module'");
}
/**
* Implements hook_uninstall().
*/
function i18n_access_uninstall() {
variable_del('i18n_access_languages');
}

View File

@@ -0,0 +1,405 @@
<?php
/**
* @file
* i18n_access.module
*/
/**
* Implements hook_user_insert().
*/
function i18n_access_user_insert(&$edit, &$account, $category = NULL) {
i18n_access_user_update($edit, $account, $category);
}
/**
* Implements hook_user_update().
*/
function i18n_access_user_update(&$edit, &$account, $category = NULL) {
if ($category == 'account') {
// see user_admin_perm_submit()
if (isset($edit['i18n_access'])) {
db_delete('i18n_access')
->condition('uid', $account->uid)
->execute();
$edit['i18n_access'] = array_filter($edit['i18n_access']);
if (count($edit['i18n_access'])) {
db_insert('i18n_access')
->fields(array(
'uid' => $account->uid,
'perm' => implode(', ', array_keys($edit['i18n_access'])),
))->execute();
}
unset($edit['i18n_access']);
}
}
}
/**
* Implements hook_user_delete().
*/
function i18n_access_user_delete($account) {
db_delete('i18n_access')
->condition('uid', $account->uid)
->execute();
}
/**
* Load the language permissions for a given user
*/
function i18n_access_load_permissions($uid = NULL) {
$perms = &drupal_static(__FUNCTION__);
// use the global user id if none is passed
if (!isset($uid)) {
$uid = $GLOBALS['user']->uid;
$account = NULL;
}
else {
$account = user_load($uid);
}
if (!isset($perms[$uid])) {
$perm_string = db_query('SELECT perm FROM {i18n_access} WHERE uid = :uid', array(':uid' => $uid))->fetchField();
if ($perm_string) {
$perms[$uid] = drupal_map_assoc(explode(', ', $perm_string));
}
else {
$perms[$uid] = array();
}
}
// adding the default languages if permission has been granted
if (user_access('access selected languages', $account)) {
$perms[$uid] = array_merge($perms[$uid], drupal_map_assoc(variable_get('i18n_access_languages', array())));
}
return $perms[$uid];
}
/**
* Implements hook_permission().
*/
function i18n_access_permission() {
return array(
'access selected languages' => array(
'title' => t('Access selected languages'),
'description' => t('This permission gives this role edit/delete access to all content which are in the <a href="!url" target="_blank">selected language</a>. View/create access needs a different access level.', array('!url' => url('admin/config/regional/language/access'))),
'restrict access' => TRUE,
),
);
}
/**
* Implements hook_form_node_form_alter().
*/
function i18n_access_form_node_form_alter(&$form) {
$form['#after_build'][] = '_i18n_access_form_node_form_alter';
}
/**
* Unset's languages from language options if user does not have permission to
* use.
*
* @param $form
* @param $form_state
* @return mixed
*/
function _i18n_access_form_node_form_alter($form, &$form_state) {
if (isset($form['language']['#options']) && !user_access('bypass node access')) {
$perms = i18n_access_load_permissions();
foreach ($form['language']['#options'] as $key => $value) {
if (empty($perms[$key])) {
unset($form['language']['#options'][$key]);
}
}
}
return $form;
}
/**
* Implements hook_form_alter().
*/
function i18n_access_form_alter(&$form, &$form_state, $form_id) {
//Configuring translation edit form to limit it to allowed language
if ($form_id == 'i18n_node_select_translation' && !user_access('bypass node access')) {
$perms = i18n_access_load_permissions();
foreach ($form['translations']['nid'] as $language => $translation) {
if (!isset($perms[$language]) && $language != '#tree') {
unset($form['translations']['nid'][$language]);
}
}
foreach ($form['translations']['language'] as $language => $translation) {
if (!isset($perms[$language]) && $language != '#tree') {
unset($form['translations']['language'][$language]);
}
}
foreach ($form['translations']['node'] as $language => $translation) {
if (!isset($perms[$language]) && $language != '#tree') {
unset($form['translations']['node'][$language]);
}
}
}
// Add i18n_access things to user/edit /user/add
if ($form_id == 'user_register_form' || $form_id == 'user_profile_form' ) {
$form['i18n_access'] = array(
'#type' => 'fieldset',
'#title' => t('Translation access'),
'#tree' => 0,
'#access' => user_access('administer users'),
);
$form['i18n_access']['i18n_access'] = array(
'#type' => 'checkboxes',
'#options' => array(LANGUAGE_NONE => t('Language neutral')) + locale_language_list('name'),
'#default_value' => i18n_access_load_permissions($form['#user']->uid),
'#description' => t('The user get edit, delete access to all content which are in this enabled languages. Create, view access needs a different access level.'),
);
}
}
/**
* Implements hook_node_access().
*/
function i18n_access_node_access($node, $op, $account = NULL, $langcode = NULL) {
// big re-work here. discarded entire original function-- replaced with our own
if (is_object($node)) {
// make sure that site administrators always have access
$permissions = i18n_access_load_permissions($user);
if (user_access('site administrator', $account)) {
return TRUE;
}
// if langcode is null it means the user is not accessing by translation overview, we throw access deny and allow to hard deny sneaky people and keep unpermitted tabs out of the menu system for the user
elseif ($langcode == NULL) {
global $language;
$langcode = $language->language;
switch ($op) {
case 'view':
return NODE_ACCESS_ALLOW;
break;
case 'update':
if (empty($permissions[$langcode])) {
return NODE_ACCESS_DENY;
}
else {
return NODE_ACCESS_ALLOW;
}
break;
case 'create':
if (empty($permissions[$langcode])) {
return NODE_ACCESS_DENY;
}
else {
return NODE_ACCESS_ALLOW;
}
break;
}
}
//if they are accessing by translation overview, the language code gets passed by the translation overview, we send true or false here
else {
switch ($op) {
case 'view':
return TRUE;
break;
case 'update':
if (empty($permissions[$langcode])) {
return FALSE;
}
else {
return TRUE;
}
break;
case 'create':
if (empty($permissions[$langcode])) {
return FALSE;
}
else {
return TRUE;
}
break;
}
}
}
}
/**
* Implements hook_menu_alter().
*/
function i18n_access_node_menu_alter(&$items) {
// due to hook_module_implementation_alter calling entity translation last, we can't change the callback here, i've done it in entity_translation.node.inc - consider calling it here?
$items['node/%node/translate']['page callback'] = 'i18n_access_translation_node_overview';
}
/**
* Most logic comes from translation/i18n_node module.
*
* We removes here only the "add translation" links for languages which are not your selected language.
*
* @see translation_node_overview
* @see i18n_node_translation_overview
*
* @param object $node
*
* @return array.
*/
function i18n_access_translation_node_overview($node) {
include_once DRUPAL_ROOT . '/includes/language.inc';
// include functions from i18n_node.pages.inc
include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'i18n_node') . '/i18n_node.pages.inc';
// this is the part where this thing sorts out how to build a list of existing translations for this node
// since we use entity translation, the tnid isn't what we're using to build the translation list. we're using node->translations->data[keys]
$available_translations = $node->translations->data;
// iterate over each available translation and add its key (which is the 2 letter language code) to the array we call $translations with the node object as the value
foreach ($available_translations as $key => $value) {
$translations[$key] = $node;
}
$header = array(t('Language'), t('Title'), t('Status'), t('Operations'));
$rows = array();
global $user;
$perms = i18n_access_load_permissions($user->uid);
//end
// Modes have different allowed languages
foreach (i18n_node_language_list($node) as $langcode => $language_name) {
if ($langcode == LANGUAGE_NONE) {
// Never show language neutral on the overview.
continue;
}
$options = array();
if (isset($translations[$langcode])) {
// Existing translation in the translation set: display status.
// We load the full node to check whether the user can edit it.
$translation_node = node_load($translations[$langcode]->nid);
$path = 'node/' . $translation_node->nid;
// Account for title field module:
if (isset($translation_node->title_field) && isset($translation_node->title_field[$langcode])) {
$title = i18n_node_translation_link($translation_node->title_field[$langcode][0]['value'], $path, $langcode);
}
else {
$title = i18n_node_translation_link($translation_node->title, $path, $langcode);
}
if (i18n_access_node_access($translation_node, 'update', $user, $langcode)) {
$text = t('edit');
$path = 'node/' . $translation_node->nid . '/edit';
$options[] = i18n_node_translation_link($text, $path, $langcode);
}
$status = $translation_node->status ? t('Published') : t('Not published');
$status .= $translation_node->translate ? ' - ' . t('outdated') . '' : '';
if ($translation_node->nid == $tnid) {
$language_name = t('@language_name (source)', array('@language_name' => $language_name));
}
}
else {
// No such translation in the set yet: help user to create it.
$title = t('n/a');
if (node_access('create', $node->type) && (!empty($perms[$langcode]) || user_access('bypass node access'))) {
$text = t('add translation');
$path = 'node/add/' . str_replace('_', '-', $node->type);
$query = array('query' => array('translation' => $node->nid, 'target' => $langcode));
$options[] = i18n_node_translation_link($text, $path, $langcode, $query);
}
$status = t('Not translated');
}
$rows[] = array($language_name, $title, $status, implode(" | ", $options));
}
drupal_set_title(t('Translations of %title', array('%title' => $node->title)), PASS_THROUGH);
$build['translation_node_overview'] = array(
'#theme' => 'table',
'#header' => $header,
'#rows' => $rows,
);
if (user_access('administer content translations')) {
$build['translation_node_select'] = drupal_get_form('i18n_node_select_translation', $node, $translations);
}
return $build;
}
/**
* Implements hook_menu().
*/
function i18n_access_menu() {
$items['admin/config/regional/language/access'] = array(
'title' => 'Access',
'page callback' => 'drupal_get_form',
'page arguments' => array('i18n_access_admin_settings'),
'access arguments' => array('administer site configuration'),
'type' => MENU_LOCAL_TASK,
'weight' => 10,
);
return $items;
}
/**
* Node-specific menu alterations.
*/
function i18n_access_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 {
$access_arguments = array(1);
}
// Point the 'translate' tab to point to the i18n_access version of the translation overview page
$items['node/%node/translate']['page callback'] = 'i18n_access_translation_node_overview';
// There are 3 page arguments for the entity translation overview, only one for i18n_access:
$items['node/%node/translate']['page arguments'] = array(1);
// Pass in the i18n_access permissions
$items['node/%node/translate']['access arguments'] = $access_arguments;
// Point to i18n_access's include for the callback
$items['node/%node/translate']['file'] = 'i18n_access.module';
// Point to i18n_access module
$items['node/%node/translate']['module'] = 'i18n_access';
}
/**
* Implements hook_module_implements_alter().
*/
function i18n_access_module_implements_alter(&$implementations, $hook) {
switch ($hook) {
case 'menu_alter':
// Move our hook_menu_alter implementation to the end of the list.
$group = $implementations['i18n_access'];
unset($implementations['i18n_access']);
$implementations['i18n_access'] = $group;
break;
}
}
/**
* Admin settings form.
*/
function i18n_access_admin_settings($form) {
$form['i18n_access_languages'] = array(
'#title' => t('Select the default access languages'),
'#type' => 'select',
'#multiple' => TRUE,
'#options' => array(LANGUAGE_NONE => t('Language neutral')) + locale_language_list('name'),
'#default_value' => variable_get('i18n_access_languages', array()),
'#description' => t("This selection of languages will be connected with the 'access selected languages' permission which you can use to grant a role access to these languages at once.")
);
return system_settings_form($form);
}

View File

@@ -0,0 +1,332 @@
<?php
/**
* @file
* Test suite for i18n_access.module
*/
class i18nAccessTestCase extends DrupalWebTestCase {
protected $admin_user;
protected $translator;
protected $visitor;
/**
* Implementation of getInfo().
*/
public static function getInfo() {
return array(
'name' => t('Translation Access'),
'description' => t('Test suite for the i18n_access module.'),
'group' => t('i18n'),
);
}
/**
* Implementation of setUp().
*/
public function setUp() {
parent::setUp(array('locale', 'translation', 'i18n_access', 'i18n_node'));
$this->admin_user = $this->drupalCreateUser(array('administer languages', 'administer site configuration', 'access administration pages', 'administer content types', 'administer users', 'bypass node access', 'translate content'));
$this->translator = $this->drupalCreateUser(array('create article content', 'edit own article content', 'translate content'));
$this->visitor = $this->drupalCreateUser(array('access content'));
$this->drupalLogin($this->admin_user);
$this->addLanguage('fr');
$this->addLanguage('de');
// Set Story content type to use multilingual support with translation.
$edit['language_content_type'] = 2;
$this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type'));
$this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Article')), 'Story content type has been updated.');
}
/**
* Enable the specified language if it has not been already.
*
* @param string $language_code
* The language code to enable.
*/
function addLanguage($language_code) {
// Check to make sure that language has not already been installed.
$this->drupalGet('admin/config/regional/language');
if (strpos($this->drupalGetContent(), 'enabled[' . $language_code . ']') === FALSE) {
// Doesn't have language installed so add it.
$edit = array();
$edit['langcode'] = $language_code;
$this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
drupal_static_reset('language_list'); // Make sure not using cached version.
$languages = language_list('language');
$this->assertTrue(array_key_exists($language_code, $languages), 'Language was installed successfully.');
if (array_key_exists($language_code, $languages)) {
$this->assertRaw(t('The language %language has been created and can now be used.', array('%language' => $languages[$language_code]->name)), 'Language has been created.');
}
}
else {
// Ensure that it is enabled.
$this->drupalPost(NULL, array('enabled[' . $language_code . ']' => TRUE), t('Save configuration'));
$this->assertRaw(t('Configuration saved.'), 'Language successfully enabled.');
}
}
/**
* Sets the language permission for the specified user. Must be logged in as
* an 'administer users' privileged user before calling this.
*
* @param $account
* The user account to modify
* @param $languages
* An array of language codes to give permission for
*/
function setLanguagePermissions($account, $languages = array()) {
$this->assertTrue(user_access('administer users'), 'User has permission to administer users');
$expected = array();
$edit = array();
foreach ($languages as $langcode) {
$key = 'i18n_access[' . $langcode . ']';
$edit[$key] = $langcode;
$expected[$langcode] = $langcode;
}
$this->drupalPost('user/' . $account->uid . '/edit', $edit, t('Save'));
$actual = i18n_access_load_permissions($account->uid);
$this->assertEqual($expected, $actual, 'Language permissions set correctly.', 'i18n_access');
}
/**
* Unsets the language permission for the specified user. Must be logged in as
* an 'administer users' privileged user before calling this.
*
* @param $account
* The user account to modify
* @param $languages
* An array of language codes to remove permission for
*/
function unsetLanguagePermissions($account, $languages = array()) {
$this->assertTrue(user_access('administer users'), 'User has permission to administer users');
$expected = array();
$edit = array();
foreach ($languages as $langcode) {
$key = 'i18n_access[' . $langcode . ']';
$edit[$key] = FALSE;
}
$this->drupalPost('user/' . $account->uid . '/edit', $edit, t('Save'));
drupal_static_reset('i18n_access_load_permissions');
drupal_static_reset('node_access');
$actual = i18n_access_load_permissions($account->uid);
$this->assertEqual($expected, $actual, 'Language permissions unset correctly.', 'i18n_access');
}
/**
* Assert that a language option exists in the language select field on the
* current page.
* @param $langcode
* Value of the language option to assert.
* @param $message
* Message to display.
* @param $group
* The group this message belongs to.
* @return
* TRUE on pass, FALSE on fail.
*/
function assertLanguageOption($langcode, $message, $group = 'Other') {
$xpath = '//select[@name="language"]/option';
$fields = $this->xpath($xpath);
// If value specified then check array for match.
$found = TRUE;
if (isset($langcode)) {
$found = FALSE;
if ($fields) {
foreach ($fields as $field) {
if ($field['value'] == $langcode) {
$found = TRUE;
}
}
}
}
return $this->assertTrue($fields && $found, $message, $group);
}
/**
* Assert that a language option does not exist in the language select field
* on the current page.
* @param $langcode
* Value of the language option to assert.
* @param $message
* Message to display.
* @param $group
* The group this message belongs to.
* @return
* TRUE on pass, FALSE on fail.
*/
function assertNoLanguageOption($langcode, $message, $group = 'Other') {
$xpath = '//select[@name="language"]/option';
$fields = $this->xpath($xpath);
// If value specified then check array for match.
$found = TRUE;
if (isset($langcode)) {
$found = FALSE;
if ($fields) {
foreach ($fields as $field) {
if ($field['value'] == $langcode) {
$found = TRUE;
}
}
}
}
return $this->assertFalse($fields && $found, $message, $group);
}
/**
* Test translator user. User with 'create article content' permission
* should be able to create and edit article nodes only in/for
* the languages that they have permissions for.
*/
function testTranslatorUser() {
$this->_testTranslatorNodeAccess();
$this->_testTranslatorNodeAccess(TRUE);
}
function _testTranslatorNodeAccess($via_role = FALSE) {
$this->drupalLogin($this->admin_user);
if (!$via_role) {
$this->setLanguagePermissions($this->translator, array('en', 'fr'));
}
else{
$edit = array(
'i18n_access_languages[]' => array('en', 'fr'),
);
$this->drupalPost('admin/config/regional/language/access', $edit, t('Save configuration'));
$this->translator = $this->drupalCreateUser(array('create article content', 'edit own article content', 'translate content', 'access selected languages'));
}
$this->drupalLogin($this->translator);
$this->drupalGet('node/add/article');
$this->assertField('language', 'Found language selector.');
$perms = i18n_access_load_permissions($this->translator->uid);
$languages = language_list();
$languages[LANGUAGE_NONE] = (object)array('language' => LANGUAGE_NONE, 'name' => 'Language Neutral');
foreach ($languages as $key => $language) {
// TODO: Add in check for language neutral
if (isset($perms[$key]) && $perms[$key]) {
$this->assertLanguageOption($language->language, format_string('Option found for %language in language selector.', array('%language' => $language->name)));
}
else {
$this->assertNoLanguageOption($language->language, format_string('Option not found for %language in language selector.', array('%language' => $language->name)));
}
}
$this->drupalLogin($this->admin_user);
$node = $this->drupalCreateNode(array('type' => 'article', 'language' => 'de', 'body' => array('de' => array(array()))));
$this->drupalLogin($this->translator);
$this->assertFalse(node_access('update', $node, $this->loggedInUser));
$this->drupalGet('node/' . $node->nid . '/edit');
$this->assertResponse(403);
$this->assertFalse(node_access('delete', $node, $this->loggedInUser));
$this->drupalGet('node/' . $node->nid . '/delete');
$this->assertResponse(403);
$this->drupalLogin($this->admin_user);
$node = $this->drupalCreateNode(array('type' => 'article', 'language' => 'fr', 'body' => array('fr' => array(array()))));
$this->drupalLogin($this->translator);
$this->assertTrue(node_access('update', $node, $this->loggedInUser));
$this->drupalGet('node/' . $node->nid . '/edit');
$this->assertResponse(200);
$this->assertTrue(node_access('delete', $node, $this->loggedInUser));
$this->drupalGet('node/' . $node->nid . '/delete');
$this->assertResponse(200);
$this->drupalGet('node/' . $node->nid . '/translate');
$query = array('query' => array('translation' => $node->nid, 'target' => 'de'));
$this->assertNoRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'de', $query));
$query = array('query' => array('translation' => $node->nid, 'target' => 'en'));
$this->assertRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'en', $query));
$this->assertRaw(i18n_node_translation_link(t('edit'), 'node/' . $node->nid . '/edit', 'fr'));
$this->drupalLogin($this->admin_user);
if (!$via_role) {
$this->unsetLanguagePermissions($this->translator, array('fr', 'en'));
}
else{
$edit = array(
'i18n_access_languages[]' => array(),
);
$this->drupalPost('admin/config/regional/language/access', $edit, t('Save configuration'));
$this->translator = $this->drupalCreateUser(array('create article content', 'edit own article content', 'translate content', 'access selected languages'));
drupal_static_reset('i18n_access_load_permissions');
drupal_static_reset('node_access');
}
$this->drupalLogin($this->translator);
$this->assertFalse(node_access('update', $node, $this->loggedInUser));
$this->drupalGet('node/' . $node->nid . '/edit');
$this->assertResponse(403);
$this->assertFalse(node_access('delete', $node, $this->loggedInUser));
$this->drupalGet('node/' . $node->nid . '/delete');
$this->assertResponse(403);
$this->drupalGet('node/' . $node->nid . '/translate');
$query = array('query' => array('translation' => $node->nid, 'target' => 'de'));
$this->assertNoRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'de', $query));
$query = array('query' => array('translation' => $node->nid, 'target' => 'en'));
$this->assertNoRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'en', $query));
$this->assertNoRaw(i18n_node_translation_link(t('edit'), 'node/' . $node->nid . '/edit', 'fr'));
}
/**
* Test admin user. User with 'bypass node access' permission should be able to
* update, delete nodes regardless of the language.
*/
function testAdminUser() {
$this->drupalLogin($this->admin_user);
$this->drupalGet('node/add/article');
$this->assertField('language', 'Found language selector.');
$languages = language_list();
$languages[LANGUAGE_NONE] = (object)array('language' => LANGUAGE_NONE, 'name' => 'Language Neutral');
foreach ($languages as $language) {
$this->assertLanguageOption($language->language, format_string('Option found for %language, regardless of permission, for administrator.', array('%language' => $language->name)));
}
$this->drupalLogin($this->translator);
$node = $this->drupalCreateNode(array('type' => 'article', 'language' => 'de', 'body' => array('de' => array(array()))));
$this->drupalLogin($this->admin_user);
$this->assertTrue(node_access('update', $node, $this->loggedInUser));
$this->drupalGet('node/' . $node->nid . '/edit');
$this->assertResponse(200);
$this->assertTrue(node_access('delete', $node, $this->loggedInUser));
$this->drupalGet('node/' . $node->nid . '/delete');
$this->assertResponse(200);
$this->drupalGet('node/' . $node->nid . '/translate');
$query = array('query' => array('translation' => $node->nid, 'target' => 'fr'));
$this->assertRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'fr', $query));
$query = array('query' => array('translation' => $node->nid, 'target' => 'en'));
$this->assertRaw(i18n_node_translation_link(t('add translation'), 'node/add/article', 'en', $query));
$this->assertRaw(i18n_node_translation_link(t('edit'), 'node/' . $node->nid . '/edit', 'de'));
}
}

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,180 @@
Translation Management Tool (tmgmt)
-------------------------------------
A collection of tools to facilitate the translation of text elements in Drupal.
Requirements
------------------
Translation Management Tool was built for Drupal 7. There will be no backport.
To use Translation Management Tool you need to install and activate the
following modules:
* Entity API
* Views
* Chaos Tools (Required for Views)
* Views Bulk Operations
* Content Translation
* Locale
* Rules
Optional dependencies:
* Internationalization/i18n
(Only necessary for i18n_string translation)
* Entity Translation (only for entity sources)
Basic concepts
------------------
With TMGMT installed, the 'translate' tab of a node changes. You can choose
one or more languages to translate the node to and 'Request a translation' with
the corresponding button.
A translation job is created for each language chosen. It will run through the
following states:
Unprocessed Translation requested in the 'translate' tab of a node.
Settings of the job (label set, translator chosen) defined.
The job was saved.
Active The job is in the process of being translated. Depending on
the chosen translator, the actual translation happens auto-
matically or by a human being.
In all cases the job is returned to the job queue for review.
When the review is done, the status of the job item goes from
'needs review' to 'accepted'.
Finished The job has been accepted and the translated node was created
The project also provides overviews for the supported sources that allow to
translate multiple pieces of content (job items) in a single job and see the
current translation status for your site content.
Getting started
------------------
The first simple translation job using Microsoft's translation service.
1) Preparation
- Make sure you have downloaded all of the listed dependencies.
- Define a second language using locale
- Modify one content type to be multilingual. Choose 'Enabled, with translation'
from the Publishing Options / Multilingual support.
2) Set up Translation Management Tool
- Download tmgmt module
- Download tmgmt_microsoft module
- Enable the following modules, this will also include all dependencies
- Translation Management UI
- Content translation Source UI
- Microsoft Translator
- A translator has been automatically created. Go to the Translator management
page at:
Configuration > Regional and language > Translation Management Translators
Adjust the label to your liking and get a client ID and client secret using
the provided link in the settings. Then save the updated translator.
- Adjust the Auto Acceptance settings to your liking. You can choose to accept
jobs without review by checking 'Auto accept finished translations' for each
of your translators individually.
3) Translate
- Create a new piece of content of the multilingual content type defined before.
Make sure to choose a language.
- Once the node has been saved, click on the "Translate" tab.
- Choose the language you want to translate the node to with the checkbox.
- Click on 'Request Translation' and the foreign language version of the node
will be created immediately.
- If the auto acceptance is not set, find the job in the jobs queue and choose
the 'review' link. Accept the translation and the translated node is created.
- Check the translated node!
For further options, see the documentation on http://drupal.org/node/1445790.
Features
----------
This projects consists of 3 major parts. The starting point are the sources,
which expose translatable content like nodes, other entities and i18n strings.
On the other side are the so called translators, which are responsible for
getting the requested sources translated.
The core system combine these two parts and provide the ability to create,
manage and review translation jobs.
The main features of the core system include:
- Creation of translations and managing their progress
- Review of returned translations, ability to request revisions and communicate
with the translator if supported.
- Translation overviews that allow to see which content is available in which
language and what translation jobs are currently ongoing.
- The same information is provided on the translate tab of the supported
sources.
- A suggestions system that makes recommendations about related content that
could be translated with the same job.
- Sources can declare which parts of a source text should not be translated,
for example placeholders for user interface strings.
The following sources are currently supported:
- Content Translation
Integrates with the core translation module to translate nodes.
- Entity Translation
Integrates with the entity_translation module that allows to translate fields
on any entity type.
- Internationalization (i18n)
Integrates with the i18n project (http://drupal.org/project/i18n) and allows
to translate various configuration elements of a site: blocks, terms, fields,
node types, contact categories and many more.
- Locale
Allows to translate locale strings. Currently limited to the default
textgroup (user interface strings passed through t()).
Two translators are included in the project:
- File translator
Allows to export jobs into files and import them once they have been
translated. Contains a pluggable system to support various file formats,
currently XLIFF and HTML.
- Local Translator
The local translator allows to manage translators on your own site so that
they can translate your content in a central place and defined workflows.
Together with the TMGMT Server, it can be used to build your own translation
server. Check the Hermes installation profile for more information:
http://drupal.org/project/hermes
Translators in separate projects:
- Microsoft Translator
Machine translation using Microsoft's Bing translation.
http://drupal.org/project/tmgmt_microsoft.
- Google
Machine translation using Google Translate.
Moved to http://drupal.org/project/tmgmt_google.
- Gengo (Previously named MyGengo)
Human translation that integrates with http://www.gengo.com.
http://drupal.org/project/tmgmt_mygengo.
- Supertext
Human translation that integrates with http://www.supertext.ch.
http://drupal.org/project/tmgmt_supertext.
- Nativy
Human translation that integrates with http://www.nativy.com.
http://drupal.org/project/tmgmt_nativy.
- One Hour Translation
Human translation that integrates with http://www.onehourtranslation.com.
http://drupal.org/project/tmgmt_oht

View File

@@ -0,0 +1,60 @@
<?php
/**
* @file
* Contains the job entity controller class.
*/
/**
* Controller class for the job entity.
*
* @ingroup tmgmt_job
*/
class TMGMTJobController extends EntityAPIController {
/**
* {@inheritdoc}
*/
public function save($entity, DatabaseTransaction $transaction = NULL) {
$entity->changed = REQUEST_TIME;
return parent::save($entity, $transaction);
}
/**
* {@inheritdoc}
*/
public function delete($ids, $transaction = NULL) {
parent::delete($ids, $transaction);
// Since we are deleting one or multiple jobs here we also need to delete
// the attached job items and messages.
$query = new EntityFieldQuery();
$result = $query->entityCondition('entity_type', 'tmgmt_job_item')
->propertyCondition('tjid', $ids)
->execute();
if (!empty($result['tmgmt_job_item'])) {
$controller = entity_get_controller('tmgmt_job_item');
// We need to directly query the entity controller so we can pass on
// the transaction object.
$controller->delete(array_keys($result['tmgmt_job_item']), $transaction);
}
$query = new EntityFieldQuery();
$result = $query->entityCondition('entity_type', 'tmgmt_message')
->propertyCondition('tjid', $ids)
->execute();
if (!empty($result['tmgmt_message'])) {
$controller = entity_get_controller('tmgmt_message');
// We need to directly query the entity controller so we can pass on
// the transaction object.
$controller->delete(array_keys($result['tmgmt_message']), $transaction);
}
$query = new EntityFieldQuery();
$result = $query->entityCondition('entity_type', 'tmgmt_remote')
->propertyCondition('tjid', $ids)
->execute();
if (!empty($result['tmgmt_remote'])) {
$controller = entity_get_controller('tmgmt_remote');
$controller->delete(array_keys($result['tmgmt_remote']), $transaction);
}
}
}

View File

@@ -0,0 +1,71 @@
<?php
/**
* @file
* Contains the job item entity controller class.
*/
/**
* Controller class for the job item entity.
*
* @ingroup tmgmt_job
*/
class TMGMTJobItemController extends EntityAPIController {
/**
* {@inheritdoc}
*
* @todo Eliminate the need to flatten and unflatten the JobItem data.
*/
public function save($entity, DatabaseTransaction $transaction = NULL) {
$entity->changed = REQUEST_TIME;
if (!empty($entity->tjid)) {
$entity->recalculateStatistics();
}
return parent::save($entity, $transaction);
}
/**
* {@inheritdoc}
*/
public function delete($ids, $transaction = NULL) {
parent::delete($ids, $transaction);
// Since we are deleting one or multiple job items here we also need to
// delete the attached messages.
$query = new EntityFieldQuery();
$result = $query->entityCondition('entity_type', 'tmgmt_message')
->propertyCondition('tjiid', $ids)
->execute();
if (!empty($result['tmgmt_message'])) {
$controller = entity_get_controller('tmgmt_message');
// We need to directly query the entity controller so we can pass on
// the transaction object.
$controller->delete(array_keys($result['tmgmt_message']), $transaction);
}
$query = new EntityFieldQuery();
$result = $query->entityCondition('entity_type', 'tmgmt_remote')
->propertyCondition('tjiid', $ids)
->execute();
if (!empty($result['tmgmt_remote'])) {
$controller = entity_get_controller('tmgmt_remote');
$controller->delete(array_keys($result['tmgmt_remote']), $transaction);
}
}
/**
* {@inheritdoc}
*/
public function invoke($hook, $entity) {
// We need to check whether the state of the job is affected by this
// deletion.
if ($hook == 'delete' && $job = $entity->getJob()) {
// We only care for active jobs.
if ($job->isActive() && tmgmt_job_check_finished($job->tjid)) {
// Mark the job as finished.
$job->finished();
}
}
parent::invoke($hook, $entity);
}
}

View File

@@ -0,0 +1,97 @@
<?php
/**
* @file
* Contains the remote controller class.
*/
/**
* Controller class for the remote job mapping entity.
*
* @ingroup tmgmt_job
*/
class TMGMTRemoteController extends EntityAPIController {
public function load($ids = array(), $conditions = array()) {
$entities = parent::load($ids, $conditions);
foreach ($entities as &$entity) {
if (is_string($entity->remote_data)) {
$entity->remote_data = unserialize($entity->remote_data);
}
}
return $entities;
}
/**
* Loads remote mappings based on local data.
*
* @param int $tjid
* Translation job id.
* @param int $tjiid
* Translation job item id.
* @param int $data_item_key
* Data item key.
*
* @return array
* Array of TMGMTRemote entities.
*/
function loadByLocalData($tjid = NULL, $tjiid = NULL, $data_item_key = NULL) {
$data_item_key = tmgmt_ensure_keys_string($data_item_key);
$query = new EntityFieldQuery();
$query->entityCondition('entity_type', 'tmgmt_remote');
if (!empty($tjid)) {
$query->propertyCondition('tjid', $tjid);
}
if (!empty($tjiid)) {
$query->propertyCondition('tjiid', $tjiid);
}
if (!empty($data_item_key)) {
$query->propertyCondition('data_item_key', $data_item_key);
}
$result = $query->execute();
if (isset($result['tmgmt_remote'])) {
return entity_load('tmgmt_remote', array_keys($result['tmgmt_remote']));
}
return array();
}
/**
* Loads remote mapping entities based on remote identifier.
*
* @param int $remote_identifier_1
* @param int $remote_identifier_2
* @param int $remote_identifier_3
*
* @return array
* Array of TMGMTRemote entities.
*/
function loadByRemoteIdentifier($remote_identifier_1 = NULL, $remote_identifier_2 = NULL, $remote_identifier_3 = NULL) {
$query = new EntityFieldQuery();
$query->entityCondition('entity_type', 'tmgmt_remote');
if ($remote_identifier_1 !== NULL) {
$query->propertyCondition('remote_identifier_1', $remote_identifier_1);
}
if ($remote_identifier_2 !== NULL) {
$query->propertyCondition('remote_identifier_2', $remote_identifier_2);
}
if ($remote_identifier_3 !== NULL) {
$query->propertyCondition('remote_identifier_3', $remote_identifier_3);
}
$result = $query->execute();
if (isset($result['tmgmt_remote'])) {
return entity_load('tmgmt_remote', array_keys($result['tmgmt_remote']));
}
return array();
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* @file
* Contains the translator controller class.
*/
/**
* Controller class for the job entity.
*
* @ingroup tmgmt_translator
*/
class TMGMTTranslatorController extends EntityAPIControllerExportable {
/**
* {@inheritdoc}
*/
protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
$query = parent::buildQuery($ids, $conditions, $revision_id);
if ($plugins = tmgmt_translator_plugin_info()) {
$query->condition('plugin', array_keys($plugins));
}
else {
// Don't return any translators if no plugin exists.
$query->where('1 = 0');
}
// Sort by the weight of the translator.
$query->orderBy('weight');
return $query;
}
/**
* {@inheritdoc}
*/
public function delete($ids, DatabaseTransaction $transaction = NULL) {
$cids = array();
// We are never going to have many entities here, so we can risk a loop.
foreach ($ids as $key => $name) {
if (tmgmt_translator_busy($key)) {
// The translator can't be deleted because it is currently busy. Remove
// it from the ids so it wont get deleted in the parent implementation.
unset($ids[$key]);
}
else {
$cids[$key] = 'language:' . $key;
}
}
// Clear the language cache for the deleted translators.
cache_clear_all($cids, 'cache_tmgmt');
parent::delete($ids, $transaction);
}
/**
* {@inheritdoc}
*/
public function save($entity, DatabaseTransaction $transaction = NULL) {
$return = parent::save($entity, $transaction);
// Clear the languages cache.
cache_clear_all('language:' . $entity->name, 'cache_tmgmt');
return $return;
}
}

View File

@@ -0,0 +1,18 @@
name = Translation Management Demo
description = All elements for a functioning demo.
package = Translation Management
core = 7.x
hidden = TRUE
dependencies[] = tmgmt_ui
dependencies[] = tmgmt_node_ui
dependencies[] = tmgmt_file
dependencies[] = tmgmt_local
dependencies[] = google_chart_tools
; Information added by Drupal.org packaging script on 2016-09-21
version = "7.x-1.0-rc2+1-dev"
core = "7.x"
project = "tmgmt"
datestamp = "1474446494"

View File

@@ -0,0 +1,89 @@
<?php
/**
* @file
* Installation hooks for tmgmt_demo module.
*/
/**
* Implements hook_install().
*/
function tmgmt_demo_install() {
include_once DRUPAL_ROOT . '/includes/locale.inc';
// Add German to the language list.
if (!array_key_exists('de', language_list())) {
locale_add_language('de');
}
// Add content type 'translatable'.
if (!array_key_exists('translatable', node_type_get_names())) {
$type = array(
'type' => 'translatable',
'name' => 'Translation Demo Type',
'base' => 'node_content',
'custom' => 1,
'modified' => 1,
'locked' => 0,
);
$type = node_type_set_defaults($type);
node_type_save($type);
node_add_body_field($type);
variable_set('language_content_type_translatable', TRUE);
variable_set('comment_translatable', '0');
}
// Add language skills to the admin user.
$user = user_load(1);
$edit = array(
'tmgmt_translation_skills' => array(
'und' => array(
0 => array(
'language_from' => 'de',
'language_to' => 'en',
),
1 => array(
'language_from' => 'en',
'language_to' => 'de',
),
),
),
);
user_save($user, $edit);
// Add demo content.
$node = new stdClass();
$node->title = 'Second node';
$node->type = 'translatable';
node_object_prepare($node);
$node->language = 'en';
$node->body[LANGUAGE_NONE][0]['value'] = 'Have another try. This text can be
translated as well';
$node->uid = $user->uid;
node_save($node);
$node = new stdClass();
$node->title = 'First node';
$node->type = 'translatable';
node_object_prepare($node);
$node->language = 'en';
$node->body[LANGUAGE_NONE][0]['value'] = 'This text can be translated with TMGMT.
Use the "translate" Tab and choose "Request Translation" to get started';
$node->uid = $user->uid;
node_save($node);
}
/**
* Implements hook_uninstall().
*/
function tmgmt_demo_uninstall() {
// Remove the content type created by the demo module.
if (array_key_exists('translatable', node_type_get_names())) {
node_type_delete('translatable');
variable_del('node_preview_translatable');
node_types_rebuild();
menu_rebuild();
}
}

View File

@@ -0,0 +1 @@
<?php

View File

@@ -0,0 +1,880 @@
<?php
/*
* @file
* Contains job entity class.
*/
/**
* Entity class for the tmgmt_job entity.
*
* @ingroup tmgmt_job
*/
class TMGMTJob extends Entity {
/**
* Translation job identifier.
*
* @var integer
*/
public $tjid;
/**
* A custom label for this job.
*/
public $label;
/**
* Current state of the translation job
* @var type
*/
public $state;
/**
* Language to be translated from.
*
* @var string
*/
public $source_language;
/**
* Language into which the data needs to be translated.
*
* @var varchar
*/
public $target_language;
/**
* Reference to the used translator of this job.
*
* @see TMGMTJob::getTranslatorController()
*
* @var string
*/
public $translator;
/**
* Translator specific configuration and context information for this job.
*
* @var array
*/
public $settings;
/**
* Remote identification of this job.
*
* @var integer
*/
public $reference;
/**
* The time when the job was created as a timestamp.
*
* @var integer
*/
public $created;
/**
* The time when the job was changed as a timestamp.
*
* @var integer
*/
public $changed;
/**
* The user id of the creator of the job.
*
* @var integer
*/
public $uid;
/**
* {@inheritdoc}
*/
public function __construct(array $values = array()) {
parent::__construct($values, 'tmgmt_job');
if (empty($this->tjid)) {
$this->created = REQUEST_TIME;
}
if (!isset($this->state)) {
$this->state = TMGMT_JOB_STATE_UNPROCESSED;
}
}
/**
* Clones job as unprocessed.
*/
public function cloneAsUnprocessed() {
$clone = clone $this;
$clone->tjid = NULL;
$clone->uid = NULL;
$clone->changed = NULL;
$clone->reference = NULL;
$clone->created = REQUEST_TIME;
$clone->state = TMGMT_JOB_STATE_UNPROCESSED;
return $clone;
}
/**
* {@inheritdoc}
*/
public function defaultLabel() {
// In some cases we might have a user-defined label.
if (!empty($this->label)) {
return $this->label;
}
$items = $this->getItems();
$count = count($items);
if ($count > 0) {
$source_label = reset($items)->getSourceLabel();
$t_args = array('!title' => $source_label, '!more' => $count - 1);
$label = format_plural($count, '!title', '!title and !more more', $t_args);
// If the label length exceeds maximum allowed then cut off exceeding
// characters from the title and use it to recreate the label.
if (strlen($label) > TMGMT_JOB_LABEL_MAX_LENGTH) {
$max_length = strlen($source_label) - (strlen($label) - TMGMT_JOB_LABEL_MAX_LENGTH);
$source_label = truncate_utf8($source_label, $max_length, TRUE);
$t_args['!title'] = $source_label;
$label = format_plural($count, '!title', '!title and !more more', $t_args);
}
}
else {
$wrapper = entity_metadata_wrapper($this->entityType, $this);
$source = $wrapper->source_language->label();
if (empty($source)) {
$source = '?';
}
$target = $wrapper->target_language->label();
if (empty($target)) {
$target = '?';
}
$label = t('From !source to !target', array('!source' => $source, '!target' => $target));
}
return $label;
}
/**
* {@inheritdoc}
*/
public function defaultUri() {
return array('path' => 'admin/tmgmt/jobs/' . $this->tjid);
}
/**
* {@inheritdoc}
*/
public function buildContent($view_mode = 'full', $langcode = NULL) {
$content = array();
if (module_exists('tmgmt_ui')) {
$content = entity_ui_get_form('tmgmt_job', $this);
}
return entity_get_controller($this->entityType)->buildContent($this, $view_mode, $langcode, $content);
}
/**
* Adds an item to the translation job.
*
* @param $plugin
* The plugin name.
* @param $item_type
* The source item type.
* @param $item_id
* The source item id.
*
* @return TMGMTJobItem
* The job item that was added to the job or FALSE if it couldn't be saved.
* @throws TMGMTException
* On zero item word count.
*/
public function addItem($plugin, $item_type, $item_id) {
$transaction = db_transaction();
$is_new = FALSE;
if (empty($this->tjid)) {
$this->save();
$is_new = TRUE;
}
$item = tmgmt_job_item_create($plugin, $item_type, $item_id, array('tjid' => $this->tjid));
$item->save();
if ($item->getWordCount() == 0) {
$transaction->rollback();
// In case we got word count 0 for the first job item, NULL tjid so that
// if there is another addItem() call the rolled back job object will get
// persisted.
if ($is_new) {
$this->tjid = NULL;
}
throw new TMGMTException('Job item @label (@type) has no translatable content.',
array('@label' => $item->label(), '@type' => $item->getSourceType()));
}
return $item;
}
/**
* Add a given TMGMTJobItem to this job.
*
* @param TMGMTJobItem $job
* The job item to add.
*/
function addExistingItem(TMGMTJobItem &$item) {
$item->tjid = $this->tjid;
$item->save();
}
/**
* Add a log message for this job.
*
* @param $message
* The message to store in the log. Keep $message translatable by not
* concatenating dynamic values into it! Variables in the message should be
* added by using placeholder strings alongside the variables argument to
* declare the value of the placeholders. See t() for documentation on how
* $message and $variables interact.
* @param $variables
* (Optional) An array of variables to replace in the message on display.
* @param $type
* (Optional) The type of the message. Can be one of 'status', 'error',
* 'warning' or 'debug'. Messages of the type 'debug' will not get printed
* to the screen.
*/
public function addMessage($message, $variables = array(), $type = 'status') {
// Save the job if it hasn't yet been saved.
if (!empty($this->tjid) || $this->save()) {
$message = tmgmt_message_create($message, $variables, array(
'tjid' => $this->tjid,
'type' => $type,
'uid' => $GLOBALS['user']->uid,
));
if ($message->save()) {
return $message;
}
}
return FALSE;
}
/**
* Returns all job items attached to this job.
*
* @param array $conditions
* Additional conditions to pass into EFQ.
*
* @return TMGMTJobItem[]
* An array of translation job items.
*/
public function getItems($conditions = array()) {
$query = new EntityFieldQuery();
$query->entityCondition('entity_type', 'tmgmt_job_item');
$query->propertyCondition('tjid', $this->tjid);
foreach ($conditions as $key => $condition) {
if (is_array($condition)) {
$operator = isset($condition['operator']) ? $condition['operator'] : '=';
$query->propertyCondition($key, $condition['value'], $operator);
}
else {
$query->propertyCondition($key, $condition);
}
}
$results = $query->execute();
if (!empty($results['tmgmt_job_item'])) {
return entity_load('tmgmt_job_item', array_keys($results['tmgmt_job_item']));
}
return array();
}
/**
* Returns all job messages attached to this job.
*
* @return array
* An array of translation job messages.
*/
public function getMessages($conditions = array()) {
$query = new EntityFieldQuery();
$query->entityCondition('entity_type', 'tmgmt_message');
$query->propertyCondition('tjid', $this->tjid);
foreach ($conditions as $key => $condition) {
if (is_array($condition)) {
$operator = isset($condition['operator']) ? $condition['operator'] : '=';
$query->propertyCondition($key, $condition['value'], $operator);
}
else {
$query->propertyCondition($key, $condition);
}
}
$results = $query->execute();
if (!empty($results['tmgmt_message'])) {
return entity_load('tmgmt_message', array_keys($results['tmgmt_message']));
}
return array();
}
/**
* Returns all job messages attached to this job with timestamp newer than
* $time.
*
* @param $time
* (Optional) Messages need to have a newer timestamp than $time. Defaults
* to REQUEST_TIME.
*
* @return array
* An array of translation job messages.
*/
public function getMessagesSince($time = NULL) {
$time = isset($time) ? $time : REQUEST_TIME;
$conditions = array('created' => array('value' => $time, 'operator' => '>='));
return $this->getMessages($conditions);
}
/**
* Retrieves a setting value from the job settings. Pulls the default values
* (if defined) from the plugin controller.
*
* @param $name
* The name of the setting.
*
* @return
* The setting value or $default if the setting value is not set. Returns
* NULL if the setting does not exist at all.
*/
public function getSetting($name) {
if (isset($this->settings[$name])) {
return $this->settings[$name];
}
// The translator might provide default settings.
if ($translator = $this->getTranslator()) {
if (($setting = $translator->getSetting($name)) !== NULL) {
return $setting;
}
}
if ($controller = $this->getTranslatorController()) {
$defaults = $controller->defaultSettings();
if (isset($defaults[$name])) {
return $defaults[$name];
}
}
}
/**
* Returns the translator for this job.
*
* @return TMGMTTranslator
* The translator entity or FALSE if there was a problem.
*/
public function getTranslator() {
if (isset($this->translator)) {
return tmgmt_translator_load($this->translator);
}
return FALSE;
}
/**
* Returns the state of the job. Can be one of the job state constants.
*
* @return integer
* The state of the job or NULL if it hasn't been set yet.
*/
public function getState() {
// We don't need to check if the state is actually set because we always set
// it in the constructor.
return $this->state;
}
/**
* Updates the state of the job.
*
* @param $state
* The new state of the job. Has to be one of the job state constants.
* @param $message
* (Optional) The log message to be saved along with the state change.
* @param $variables
* (Optional) An array of variables to replace in the message on display.
*
* @return int
* The updated state of the job if it could be set.
*
* @see TMGMTJob::addMessage()
*/
public function setState($state, $message = NULL, $variables = array(), $type = 'debug') {
// Return TRUE if the state could be set. Return FALSE otherwise.
if (array_key_exists($state, tmgmt_job_states())) {
$this->state = $state;
$this->save();
// If a message is attached to this state change add it now.
if (!empty($message)) {
$this->addMessage($message, $variables, $type);
}
}
return $this->state;
}
/**
* Checks whether the passed value matches the current state.
*
* @param $state
* The value to check the current state against.
*
* @return boolean
* TRUE if the passed state matches the current state, FALSE otherwise.
*/
public function isState($state) {
return $this->getState() == $state;
}
/**
* Checks whether the user described by $account is the author of this job.
*
* @param $account
* (Optional) A user object. Defaults to the currently logged in user.
*/
public function isAuthor($account = NULL) {
$account = isset($account) ? $account : $GLOBALS['user'];
return $this->uid == $account->uid;
}
/**
* Returns whether the state of this job is 'unprocessed'.
*
* @return boolean
* TRUE if the state is 'unprocessed', FALSE otherwise.
*/
public function isUnprocessed() {
return $this->isState(TMGMT_JOB_STATE_UNPROCESSED);
}
/**
* Returns whether the state of this job is 'aborted'.
*
* @return boolean
* TRUE if the state is 'aborted', FALSE otherwise.
*/
public function isAborted() {
return $this->isState(TMGMT_JOB_STATE_ABORTED);
}
/**
* Returns whether the state of this job is 'active'.
*
* @return boolean
* TRUE if the state is 'active', FALSE otherwise.
*/
public function isActive() {
return $this->isState(TMGMT_JOB_STATE_ACTIVE);
}
/**
* Returns whether the state of this job is 'rejected'.
*
* @return boolean
* TRUE if the state is 'rejected', FALSE otherwise.
*/
public function isRejected() {
return $this->isState(TMGMT_JOB_STATE_REJECTED);
}
/**
* Returns whether the state of this jon is 'finished'.
*
* @return boolean
* TRUE if the state is 'finished', FALSE otherwise.
*/
public function isFinished() {
return $this->isState(TMGMT_JOB_STATE_FINISHED);
}
/**
* Checks whether a job is translatable.
*
* @return boolean
* TRUE if the job can be translated, FALSE otherwise.
*/
public function isTranslatable() {
if ($translator = $this->getTranslator()) {
if ($translator->canTranslate($this)) {
return TRUE;
}
}
return FALSE;
}
/**
* Checks whether a job is abortable.
*
* @return boolean
* TRUE if the job can be aborted, FALSE otherwise.
*/
public function isAbortable() {
// Only non-submitted translation jobs can be aborted.
return $this->isActive();
}
/**
* Checks whether a job is submittable.
*
* @return boolean
* TRUE if the job can be submitted, FALSE otherwise.
*/
public function isSubmittable() {
return $this->isUnprocessed() || $this->isRejected();
}
/**
* Checks whether a job is deletable.
*
* @return boolean
* TRUE if the job can be deleted, FALSE otherwise.
*/
public function isDeletable() {
return !$this->isActive();
}
/**
* Set the state of the job to 'submitted'.
*
* @param $message
* The log message to be saved along with the state change.
* @param $variables
* (Optional) An array of variables to replace in the message on display.
*
* @return TMGMTJob
* The job entity.
*
* @see TMGMTJob::addMessage()
*/
public function submitted($message = NULL, $variables = array(), $type = 'status') {
if (!isset($message)) {
$message = 'The translation job has been submitted.';
}
$this->setState(TMGMT_JOB_STATE_ACTIVE, $message, $variables, $type);
}
/**
* Set the state of the job to 'finished'.
*
* @param $message
* The log message to be saved along with the state change.
* @param $variables
* (Optional) An array of variables to replace in the message on display.
*
* @return TMGMTJob
* The job entity.
*
* @see TMGMTJob::addMessage()
*/
public function finished($message = NULL, $variables = array(), $type = 'status') {
if (!isset($message)) {
$message = 'The translation job has been finished.';
}
return $this->setState(TMGMT_JOB_STATE_FINISHED, $message, $variables, $type);
}
/**
* Sets the state of the job to 'aborted'.
*
* @param $message
* The log message to be saved along with the state change.
* @param $variables
* (Optional) An array of variables to replace in the message on display.
*
* Use TMGMTJob::abortTranslation() to abort a translation.
*
* @return TMGMTJob
* The job entity.
*
* @see TMGMTJob::addMessage()
*/
public function aborted($message = NULL, $variables = array(), $type = 'status') {
if (!isset($message)) {
$message = 'The translation job has been aborted.';
}
/** @var TMGMTJobItem $item */
foreach ($this->getItems() as $item) {
$item->setState(TMGMT_JOB_ITEM_STATE_ABORTED);
}
return $this->setState(TMGMT_JOB_STATE_ABORTED, $message, $variables, $type);
}
/**
* Sets the state of the job to 'rejected'.
*
* @param $message
* The log message to be saved along with the state change.
* @param $variables
* (Optional) An array of variables to replace in the message on display.
*
* @return TMGMTJob
* The job entity.
*
* @see TMGMTJob::addMessage()
*/
public function rejected($message = NULL, $variables = array(), $type = 'error') {
if (!isset($message)) {
$message = 'The translation job has been rejected by the translation provider.';
}
return $this->setState(TMGMT_JOB_STATE_REJECTED, $message, $variables, $type);
}
/**
* Request the translation of a job from the translator.
*
* @return integer
* The updated job status.
*/
public function requestTranslation() {
if (!$this->isTranslatable() || !$controller = $this->getTranslatorController()) {
return FALSE;
}
// We don't know if the translator plugin already processed our
// translation request after this point. That means that the plugin has to
// set the 'submitted', 'needs review', etc. states on its own.
$controller->requestTranslation($this);
}
/**
* Attempts to abort the translation job. Already accepted jobs can not be
* aborted, submitted jobs only if supported by the translator plugin.
* Always use this method if you want to abort a translation job.
*
* @return boolean
* TRUE if the translation job was aborted, FALSE otherwise.
*/
public function abortTranslation() {
if (!$this->isAbortable() || !$controller = $this->getTranslatorController()) {
return FALSE;
}
// We don't know if the translator plugin was able to abort the translation
// job after this point. That means that the plugin has to set the
// 'aborted' state on its own.
return $controller->abortTranslation($this);
}
/**
* Returns the translator plugin controller of the translator of this job.
*
* @return TMGMTTranslatorPluginControllerInterface
* The controller of the translator plugin.
*/
public function getTranslatorController() {
if ($translator = $this->getTranslator($this)) {
return $translator->getController();
}
return FALSE;
}
/**
* Returns the source data of all job items.
*
* @param $key
* If present, only the subarray identified by key is returned.
* @param $index
* Optional index of an attribute below $key.
* @return array
* A nested array with the source data where the most upper key is the job
* item id.
*/
public function getData(array $key = array(), $index = NULL) {
$data = array();
if (!empty($key)) {
$tjiid = array_shift($key);
$item = entity_load_single('tmgmt_job_item', $tjiid);
if ($item) {
$data[$tjiid] = $item->getData($key, $index);
// If not set, use the job item label as the data label.
if (!isset($data[$tjiid]['#label'])) {
$data[$tjiid]['#label'] = $item->getSourceLabel();
}
}
}
else {
foreach ($this->getItems() as $tjiid => $item) {
$data[$tjiid] = $item->getData();
// If not set, use the job item label as the data label.
if (!isset($data[$tjiid]['#label'])) {
$data[$tjiid]['#label'] = $item->getSourceLabel();
}
}
}
return $data;
}
/**
* Sums up all pending counts of this jobs job items.
*
* @return
* The sum of all pending counts
*/
public function getCountPending() {
return tmgmt_job_statistic($this, 'count_pending');
}
/**
* Sums up all translated counts of this jobs job items.
*
* @return
* The sum of all translated counts
*/
public function getCountTranslated() {
return tmgmt_job_statistic($this, 'count_translated');
}
/**
* Sums up all accepted counts of this jobs job items.
*
* @return
* The sum of all accepted data items.
*/
public function getCountAccepted() {
return tmgmt_job_statistic($this, 'count_accepted');
}
/**
* Sums up all accepted counts of this jobs job items.
*
* @return
* The sum of all accepted data items.
*/
public function getCountReviewed() {
return tmgmt_job_statistic($this, 'count_reviewed');
}
/**
* Sums up all word counts of this jobs job items.
*
* @return
* The total word count of this job.
*/
public function getWordCount() {
return tmgmt_job_statistic($this, 'word_count');
}
/**
* Store translated data back into the items.
*
* @param $data
* Partially or complete translated data, the most upper key needs to be
* the translation job item id.
* @param $key
* (Optional) Either a flattened key (a 'key1][key2][key3' string) or a nested
* one, e.g. array('key1', 'key2', 'key2'). Defaults to an empty array which
* means that it will replace the whole translated data array. The most
* upper key entry needs to be the job id (tjiid).
*/
public function addTranslatedData($data, $key = NULL) {
$key = tmgmt_ensure_keys_array($key);
$items = $this->getItems();
// If there is a key, get the specific item and forward the call.
if (!empty($key)) {
$item_id = array_shift($key);
if (isset($items[$item_id])) {
$items[$item_id]->addTranslatedData($data, $key);
}
}
else {
foreach ($data as $key => $value) {
if (isset($items[$key])) {
$items[$key]->addTranslatedData($value);
}
}
}
}
/**
* Propagates the returned job item translations to the sources.
*
* @return boolean
* TRUE if we were able to propagate the translated data, FALSE otherwise.
*/
public function acceptTranslation() {
foreach ($this->getItems() as $item) {
$item->acceptTranslation();
}
}
/**
* Gets remote mappings for current job.
*
* @return array
* List of TMGMTRemote entities.
*/
public function getRemoteMappings() {
$query = new EntityFieldQuery();
$query->entityCondition('entity_type', 'tmgmt_remote');
$query->propertyCondition('tjid', $this->tjid);
$result = $query->execute();
if (isset($result['tmgmt_remote'])) {
return entity_load('tmgmt_remote', array_keys($result['tmgmt_remote']));
}
return array();
}
/**
* Invoke the hook 'hook_tmgmt_source_suggestions' to get all suggestions.
*
* @param arary $conditions
* Conditions to pass only some and not all items to the hook.
*
* @return array
* An array with all additional translation suggestions.
* - job_item: A TMGMTJobItem instance.
* - referenced: A string which indicates where this suggestion comes from.
* - from_job: The main TMGMTJob-ID which suggests this translation.
*/
public function getSuggestions(array $conditions = array()) {
$suggestions = module_invoke_all('tmgmt_source_suggestions', $this->getItems($conditions), $this);
// Each TMGMTJob needs a job id to be able to count the words, because the
// source-language is stored in the job and not the item.
foreach ($suggestions as &$suggestion) {
$jobItem = $suggestion['job_item'];
$jobItem->tjid = $this->tjid;
$jobItem->recalculateStatistics();
}
return $suggestions;
}
/**
* Removes all suggestions from the given list which should not be processed.
*
* This function removes all suggestions from the given list which are already
* assigned to a translation job or which should not be processed because
* there are no words, no translation is needed, ...
*
* @param array &$suggestions
* Associative array of translation suggestions. It must contain at least:
* - tmgmt_job: An instance of a TMGMTJobItem.
*/
public function cleanSuggestionsList(array &$suggestions) {
foreach ($suggestions as $k => $suggestion) {
if (is_array($suggestion) && isset($suggestion['job_item']) && ($suggestion['job_item'] instanceof TMGMTJobItem)) {
$jobItem = $suggestion['job_item'];
// Items with no words to translate should not be presented.
if ($jobItem->getWordCount() <= 0) {
unset($suggestions[$k]);
continue;
}
// Check if there already exists a translation job for this item in the
// current language.
$items = tmgmt_job_item_load_all_latest($jobItem->plugin, $jobItem->item_type, $jobItem->item_id, $this->source_language);
if ($items && isset($items[$this->target_language])) {
unset($suggestions[$k]);
continue;
}
} else {
unset($suggestions[$k]);
continue;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,143 @@
<?php
/*
* @file
* Contains message entity class.
*/
/**
* Entity class for the tmgmt_message entity.
*
* @ingroup tmgmt_job
*/
class TMGMTMessage extends Entity {
/**
* The ID of the message..
*
* @var integer
*/
public $mid;
/**
* The ID of the job.
*
* @var integer
*/
public $tjid;
/**
* The ID of the job item.
*
* @var integer
*/
public $tjiid;
/**
* User uid.
*
* @var integer
*/
public $uid;
/**
* The message text.
*
* @var string
*/
public $message;
/**
* An array of string replacement arguments as used by t().
*
* @var array
*/
public $variables;
/**
* The time when the message object was created as a timestamp.
*
* @var integer
*/
public $created;
/**
* Type of the message (debug, status, warning or error).
*
* @var string
*/
public $type;
/**
* {@inheritdoc}
*/
public function __construct(array $values = array()) {
parent::__construct($values, 'tmgmt_message');
if (empty($this->created)) {
$this->created = REQUEST_TIME;
}
if (empty($this->type)) {
$this->type = 'status';
}
}
/**
* {@inheritdoc}
*/
public function defaultLabel() {
$created = format_date($this->created);
switch ($this->type) {
case 'error':
return t('Error message from @time', array('@time' => $created));
case 'status':
return t('Status message from @time', array('@time' => $created));
case 'warning':
return t('Warning message from @time', array('@time' => $created));
case 'debug':
return t('Debug message from @time', array('@time' => $created));
}
}
/**
* Returns the translated message.
*
* @return
* The translated message.
*/
public function getMessage() {
$text = $this->message;
if (is_array($this->variables) && !empty($this->variables)) {
$text = t($text, $this->variables);
}
return $text;
}
/**
* Loads the job entity that this job message is attached to.
*
* @return TMGMTJob
* The job entity that this job message is attached to or FALSE if there was
* a problem.
*/
public function getJob() {
if (!empty($this->tjid)) {
return tmgmt_job_load($this->tjid);
}
return FALSE;
}
/**
* Loads the job entity that this job message is attached to.
*
* @return TMGMTJobItem
* The job item entity that this job message is attached to or FALSE if
* there was a problem.
*/
public function getJobItem() {
if (!empty($this->tjiid)) {
return tmgmt_job_item_load($this->tjiid);
}
return FALSE;
}
}

View File

@@ -0,0 +1,155 @@
<?php
/*
* @file
* Contains remote entity class.
*/
/**
* Entity class for the tmgmt_remote entity.
*
* @ingroup tmgmt_job
*/
class TMGMTRemote extends Entity {
/**
* Primary key.
*
* @var int
*/
public $trid;
/**
* TMGMTJob identifier.
*
* @var int
*/
public $tjid;
/**
* TMGMTJobItem identifier.
*
* @var int
*/
public $tjiid;
/**
* Translation job data item key.
*
* @var string
*/
public $data_item_key;
/**
* Custom remote identifier 1.
*
* @var string
*/
public $remote_identifier_1;
/**
* Custom remote identifier 2.
*
* @var string
*/
public $remote_identifier_2;
/**
* Custom remote identifier 3.
*
* @var string
*/
public $remote_identifier_3;
/**
* Remote job url.
*
* @var string
*/
public $remote_url;
/**
* Word count provided by the remote service.
*
* @var int
*/
public $word_count;
/**
* Amount charged for the remote translation job.
*
* @var int
*/
public $amount;
/**
* Amount charged currency.
*
* @var string
*/
public $currency;
/**
* Custom remote data.
*
* @var array
*/
public $remote_data;
/**
* Gets translation job.
*
* @return TMGMTJob
*/
function getJob() {
return tmgmt_job_load($this->tjid);
}
/**
* Gets translation job item.
*
* @return TMGMTJobItem
*/
function getJobItem() {
if (!empty($this->tjiid)) {
return tmgmt_job_item_load($this->tjiid);
}
return NULL;
}
/**
* Adds data to the remote_data storage.
*
* @param string $key
* Key through which the data will be accessible.
* @param $value
* Value to store.
*/
function addRemoteData($key, $value) {
$this->remote_data[$key] = $value;
}
/**
* Gets data from remote_data storage.
*
* @param string $key
* Access key for the data.
*
* @return mixed
* Stored data.
*/
function getRemoteData($key) {
return $this->remote_data[$key];
}
/**
* Removes data from remote_data storage.
*
* @param string $key
* Access key for the data that are to be removed.
*/
function removeRemoteData($key) {
unset($this->remote_data[$key]);
}
}

View File

@@ -0,0 +1,299 @@
<?php
/*
* @file
* Contains translator entity class.
*/
/**
* Entity class for the tmgmt_translator entity.
*
* @ingroup tmgmt_translator
*/
class TMGMTTranslator extends Entity {
/**
* The ID of the translator.
*
* @var integer
*/
public $tid;
/**
* Machine readable name of the translator.
*
* @var string
*/
public $name;
/**
* Label of the translator.
*
* @var string
*/
public $label;
/**
* Description of the translator.
*
* @var string
*/
public $description;
/**
* Weight of the translator.
*
* @var int
*/
public $weight;
/**
* Plugin name of the translator.
*
* @type string
*/
public $plugin;
/**
* Translator type specific settings.
*
* @var array
*/
public $settings;
/**
* The supported target languages caches.
*
* @var array
*/
protected $languageCache;
/**
* The supported language pairs caches.
*
* @var array
*/
protected $languagePairsCache;
/**
* Whether the language cache in the database is outdated.
*
* @var boolean
*/
protected $languageCacheOutdated;
/**
* {@inheritdoc}
*/
public function __construct(array $values = array()) {
parent::__construct($values, 'tmgmt_translator');
}
/**
* Returns the translator plugin controller of this translator.
*
* @return TMGMTTranslatorPluginControllerInterface
*/
public function getController() {
if (!empty($this->plugin)) {
return tmgmt_translator_plugin_controller($this->plugin);
}
return FALSE;
}
/**
* Returns the supported target languages for this translator.
*
* @return array
* An array of supported target languages in ISO format.
*/
public function getSupportedTargetLanguages($source_language) {
if ($controller = $this->getController()) {
if (isset($this->pluginInfo['cache languages']) && empty($this->pluginInfo['cache languages'])) {
// This plugin doesn't support language caching.
return $controller->getSupportedTargetLanguages($this, $source_language);
}
else {
// Retrieve the supported languages from the cache.
if (empty($this->languageCache) && $cache = cache_get('languages:' . $this->name, 'cache_tmgmt')) {
$this->languageCache = $cache->data;
}
// Even if we successfully queried the cache it might not have an entry
// for our source language yet.
if (!isset($this->languageCache[$source_language])) {
$this->languageCache[$source_language] = $controller->getSupportedTargetLanguages($this, $source_language);
$this->languageCacheOutdated = TRUE;
}
}
return $this->languageCache[$source_language];
}
}
/**
* Gets the supported language pairs for this translator.
*
* @return array
* List of language pairs where a pair is an associative array of
* source_language and target_language.
* Example:
* array(
* array('source_language' => 'en-US', 'target_language' => 'de-DE'),
* array('source_language' => 'en-US', 'target_language' => 'de-CH'),
* )
*/
public function getSupportedLanguagePairs() {
if ($controller = $this->getController()) {
if (isset($this->pluginInfo['cache languages']) && empty($this->pluginInfo['cache languages'])) {
// This plugin doesn't support language caching.
return $controller->getSupportedLanguagePairs($this);
}
else {
// Retrieve the supported languages from the cache.
if (empty($this->languagePairsCache) && $cache = cache_get('language_pairs:' . $this->name, 'cache_tmgmt')) {
$this->languagePairsCache = $cache->data;
}
// Even if we successfully queried the cache data might not be yet
// available.
if (empty($this->languagePairsCache)) {
$this->languagePairsCache = $controller->getSupportedLanguagePairs($this);
$this->languageCacheOutdated = TRUE;
}
}
return $this->languagePairsCache;
}
}
/**
* Clears the language cache for this translator.
*/
public function clearLanguageCache() {
cache_clear_all('languages:' . $this->name, 'cache_tmgmt');
cache_clear_all('language_pairs:' . $this->name, 'cache_tmgmt');
}
/**
* Check whether this translator can handle a particular translation job.
*
* @param $job
* The TMGMTJob entity that should be translated.
*
* @return boolean
* TRUE if the job can be processed and translated, FALSE otherwise.
*/
public function canTranslate(TMGMTJob $job) {
if ($controller = $this->getController()) {
return $controller->canTranslate($this, $job);
}
return FALSE;
}
/**
* Checks whether a translator is available.
*
* @return boolean
* TRUE if the translator plugin is available, FALSE otherwise.
*/
public function isAvailable() {
if ($controller = $this->getController()) {
return $controller->isAvailable($this);
}
return FALSE;
}
/**
* Returns if the plugin has any settings for this job.
*/
public function hasCheckoutSettings(TMGMTJob $job) {
if ($controller = $this->getController()) {
return $controller->hasCheckoutSettings($job);
}
return FALSE;
}
/**
* @todo Remove this once http://drupal.org/node/1420364 is done.
*/
public function getNotAvailableReason() {
if ($controller = $this->getController()) {
return $controller->getNotAvailableReason($this);
}
return FALSE;
}
/**
* @todo Remove this once http://drupal.org/node/1420364 is done.
*/
public function getNotCanTranslateReason(TMGMTJob $job) {
if ($controller = $this->getController()) {
return $controller->getNotCanTranslateReason($job);
}
return FALSE;
}
/**
* Retrieves a setting value from the translator settings. Pulls the default
* values (if defined) from the plugin controller.
*
* @param $name
* The name of the setting.
*
* @return
* The setting value or $default if the setting value is not set. Returns
* NULL if the setting does not exist at all.
*/
public function getSetting($name) {
if (isset($this->settings[$name])) {
return $this->settings[$name];
}
elseif ($controller = $this->getController()) {
$defaults = $controller->defaultSettings();
if (isset($defaults[$name])) {
return $defaults[$name];
}
}
}
/**
* Maps local language to remote language.
*
* @param $language
* Local language code.
*
* @return string
* Remote language code.
*
* @ingroup tmgmt_remote_languages_mapping
*/
public function mapToRemoteLanguage($language) {
return $this->getController()->mapToRemoteLanguage($this, $language);
}
/**
* Maps remote language to local language.
*
* @param $language
* Remote language code.
*
* @return string
* Local language code.
*
* @ingroup tmgmt_remote_languages_mapping
*/
public function mapToLocalLanguage($language) {
return $this->getController()->mapToLocalLanguage($this, $language);
}
/**
* Updates the language cache if it has changed.
*/
public function __destruct() {
if ($controller = $this->getController()) {
$info = $controller->pluginInfo();
if (!isset($info['language cache']) || !empty($info['language cache']) && !empty($this->languageCacheOutdated)) {
cache_set('languages:' . $this->name, $this->languageCache, 'cache_tmgmt');
cache_set('language_pairs:' . $this->name, $this->languagePairsCache, 'cache_tmgmt');
}
}
}
}

View File

@@ -0,0 +1,17 @@
<?php
/**
* TMGMT Exception class
*/
class TMGMTException extends Exception {
/**
* @param string $message
* @param array $data
* Associative array of dynamic data that will be inserted into $message.
* @param int $code
*/
function __construct($message = "", $data = array(), $code = 0) {
parent::__construct(strtr($message, $data), $code);
}
}

View File

@@ -0,0 +1,174 @@
<?php
/**
* @file
* Contains the metadata controller classes for the Translation Management Tool
* entities.
*/
/**
* Metadata controller for the job entity.
*/
class TMGMTJobMetadataController extends EntityDefaultMetadataController {
/**
* {@inheritdoc}
*/
public function entityPropertyInfo() {
$info = parent::entityPropertyInfo();
$info = _tmgmt_override_property_description($info, $this->type);
$properties = &$info[$this->type]['properties'];
// Make the created and changed property appear as date.
$properties['changed']['type'] = $properties['created']['type'] = 'date';
// Use the defined entity label callback instead of the custom label directly.
$properties['label']['getter callback'] = 'entity_class_label';
// Allow to change the properties.
foreach (array('target_language', 'source_language', 'translator') as $property) {
$properties[$property]['setter callback'] = 'entity_property_verbatim_set';
}
// Add the options list for the available languages.
$properties['target_language']['options list'] = $properties['source_language']['options list'] = 'entity_metadata_language_list';
// Add the options list for the defined state constants.
$properties['state']['options list'] = 'tmgmt_job_states';
// Add the options list for all available translator plugins.
$properties['translator']['type'] = 'tmgmt_translator';
$properties['translator']['options list'] = 'tmgmt_translator_labels';
// Link the author property to the corresponding user entity.
$properties['author'] = array(
'label' => t('Author'),
'type' => 'user',
'description' => t('The author of the translation job.'),
'setter callback' => 'entity_property_verbatim_set',
'setter permission' => 'administer tmgmt',
'required' => TRUE,
'schema field' => 'uid',
);
return $info;
}
}
/**
* Metadata controller for the job item entity.
*/
class TMGMTJobItemMetadataController extends EntityDefaultMetadataController {
public function entityPropertyInfo() {
$info = parent::entityPropertyInfo();
$info = _tmgmt_override_property_description($info, $this->type);
$properties = &$info[$this->type]['properties'];
// Make the created and changed property appear as date.
$properties['changed']['type'] = 'date';
// Add the options list for the defined state constants.
$properties['state']['options list'] = 'tmgmt_job_item_states';
// Link the job id property to the corresponding job entity.
$properties['tjid'] = array(
'description' => t('Corresponding job entity.'),
'type' => 'tmgmt_job',
) + $properties['tjid'];
// Add the options list for all available source plugins.
$properties['plugin']['options list'] = 'tmgmt_source_plugin_labels';
$properties['word_count']['label'] = t('Word count');
return $info;
}
}
/**
* Metadata controller for the job message entity.
*/
class TMGMTMessageMetadataController extends EntityDefaultMetadataController {
/**
* {@inheritdoc}
*/
public function entityPropertyInfo() {
$info = parent::entityPropertyInfo();
$info = _tmgmt_override_property_description($info, $this->type);
$properties = &$info[$this->type]['properties'];
// Make the created property appear as date.
$properties['created']['type'] = 'date';
// Link the job id property to the corresponding job entity.
$properties['tjid'] = array(
'description' => t('Corresponding job entity.'),
'type' => 'tmgmt_job',
) + $properties['tjid'];
// Link the job item id property to the corresponding job item entity.
$properties['tjiid'] = array(
'description' => t('Corresponding job item entity.'),
'type' => 'tmgmt_job_item',
) + $properties['tjiid'];
// Link user, was added in an update so make sure that it doesn't explode
// if the schema cache was not cleared.
$properties['uid'] = array(
'type' => 'user',
'description' => t('User associated with TMGMT Job Message entity.'),
) + (isset($properties['uid']) ? $properties['uid'] : array());
return $info;
}
}
/**
* Metadata controller for the translator entity.
*/
class TMGMTTranslatorMetadataController extends EntityDefaultMetadataController {
/**
* {@inheritdoc}
*/
public function entityPropertyInfo() {
$info = parent::entityPropertyInfo();
$info = _tmgmt_override_property_description($info, $this->type);
$properties = &$info[$this->type]['properties'];
// Options list callback for the translator plugin labels.
$properties['plugin']['options list'] = 'tmgmt_translator_plugin_labels';
return $info;
}
}
/**
* Populates all entity property descriptions based on the schema definition.
*
* @param $info
* Entity propety info array.
*
* @return
* The altered entity properties array.
*/
function _tmgmt_override_property_description($info, $entity_type) {
// Load tmgmt.install so we can access the schema.
module_load_install('tmgmt');
$entity_info = entity_get_info($entity_type);
$schema = tmgmt_schema();
$fields = $schema[$entity_info['base table']]['fields'];
$properties = &$info[$entity_type]['properties'];
foreach ($properties as $name => $property_info) {
if (isset($fields[$name]['description'])) {
$properties[$name]['description'] = $fields[$name]['description'];
}
}
return $info;
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* @file
* Contains the base plugin class.
*/
/**
* Base class for Translation Management plugins.
*/
class TMGMTPluginBase implements TMGMTPluginBaseInterface {
protected $pluginType;
protected $pluginInfo;
/**
* {@inheritdoc}
*/
public function __construct($type, $plugin) {
$this->pluginType = $plugin;
$this->pluginInfo = _tmgmt_plugin_info($type, $plugin);
}
/**
* {@inheritdoc}
*/
public function pluginInfo() {
return $this->pluginInfo;
}
/**
* {@inheritdoc}
*/
public function pluginType() {
return $this->pluginType;
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* @file
* Contains the base plugin interface.
*/
/**
* Base interface for Translation Management plugins.
*/
interface TMGMTPluginBaseInterface {
/**
* Constructor.
*
* @param $type
* The plugin type.
* @param $plugin
* The machine-readable name of the plugin.
*/
public function __construct($type, $plugin);
/**
* Returns the info of the type of the plugin.
*
* @see tmgmt_source_plugin_info()
*/
public function pluginInfo();
/**
* Returns the type of the plugin.
*/
public function pluginType();
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* @file
* Contains the reject translator plugin interface.
*/
/**
* Handle reject on data item level.
*
* Implement this interface in a translator plugin to signal that this plugin is
* capable of handling a reject of single data items.
*
* @ingroup tmgmt_translator
*/
interface TMGMTTranslatorRejectDataItem {
/**
* Reject one single data item.
*
* @todo Using job item breaks the current convention which uses jobs.
*
* @param $job_item
* The job item to which the rejected data item belongs.
* @param $key
* The key of the rejected data item.
* The key is an array containing the keys of a nested array hierarchy path.
*
* @return
* TRUE if the reject was succesfull, else FALSE.
* In case of an error, it is the responsibility of the translator to
* provide informations about the faliure.
*/
public function rejectDataItem(TMGMTJobItem $job_item, array $key, array $values = NULL);
/**
* Reject form.
*
* This method gets call by tmgmt_ui_translation_review_form_reject_confirm
* and allows the translator to add aditional form elements in order to
* collect data needed for the reject prozess.
*
* @param $form
* The form array containing a confirm form.
* $form['item'] holds the job item to which the to be rejected data item
* belongs to.
* $form['item'] holds key of the to be rejected data item as an array of
* keys of a nested array hierarchy.
* @param $form_state
* The form state.
*
* @return
* The resulting form array.
*/
public function rejectForm($form, &$form_state);
}

View File

@@ -0,0 +1,103 @@
<?php
/**
* @file
* Contains the source plugin interface.
*/
/**
* Interface for source plugin controllers.
*
* @ingroup tmgmt_source
*/
interface TMGMTSourcePluginControllerInterface extends TMGMTPluginBaseInterface {
/**
* Returns an array with the data structured for translation.
*
* @param TMGMTJobItem $job_item
* The job item entity.
*
* @see TMGMTJobItem::getData()
*/
public function getData(TMGMTJobItem $job_item);
/**
* Saves a translation.
*
* @param TMGMTJobItem $job_item
* The job item entity.
*
* @return boolean
* TRUE if the translation was saved successfully, FALSE otherwise.
*/
public function saveTranslation(TMGMTJobItem $job_item);
/**
* Return a title for this job item.
*
* @param TMGMTJobItem $job_item
* The job item entity.
*/
public function getLabel(TMGMTJobItem $job_item);
/**
* Returns the Uri for this job item.
*
* @param TMGMTJobItem $job_item
* The job item entity.
*
* @see entity_uri()
*/
public function getUri(TMGMTJobItem $job_item);
/**
* Returns an array of translatable source item types.
*/
public function getItemTypes();
/**
* Returns the label of a source item type.
*
* @param $type
* The identifier of a source item type.
*/
public function getItemTypeLabel($type);
/**
* Returns the type of a job item.
*
* @param TMGMTJobItem $job_item
* The job item.
*
* @return string
* A type that describes the job item.
*/
public function getType(TMGMTJobItem $job_item);
/**
* Gets language code of the job item source.
*
* @param TMGMTJobItem $job_item
* The job item.
*
* @return string
* Language code.
*/
public function getSourceLangCode(TMGMTJobItem $job_item);
/**
* Gets existing translation language codes of the job item source.
*
* Returns language codes that can be used as the source language for a
* translation job.
*
* @param TMGMTJobItem $job_item
* The job item.
*
* @return array
* Array of language codes.
*/
public function getExistingLangCodes(TMGMTJobItem $job_item);
}

View File

@@ -0,0 +1,258 @@
<?php
/**
* @file
* Contains the source plugin interface.
*/
/**
* Interface for service plugin controllers.
*
* @ingroup tmgmt_translator
*/
interface TMGMTTranslatorPluginControllerInterface extends TMGMTPluginBaseInterface {
/**
* Checks whether a translator is available.
*
* @param TMGMTTranslator $translator
* The translator entity.
*
* @return boolean
* TRUE if the translator plugin is available, FALSE otherwise.
*/
public function isAvailable(TMGMTTranslator $translator);
/**
* Return a reason why the translator is not available.
*
* @param TMGMTTranslator $translator
* The translator entity.
*
* Might be called when isAvailable() returns FALSE to get a reason that
* can be displayed to the user.
*
* @todo Remove this once http://drupal.org/node/1420364 is done.
*/
public function getNotAvailableReason(TMGMTTranslator $translator);
/**
* Check whether this service can handle a particular translation job.
*
* @param TMGMTTranslator $translator
* The TMGMTTranslator entity that should handle the translation.
* @param TMGMTJob $job
* The TMGMTJob entity that should be translated.
*
* @return boolean
* TRUE if the job can be processed and translated, FALSE otherwise.
*/
public function canTranslate(TMGMTTranslator $translator, TMGMTJob $job);
/**
* Return a reason why the translator is not able to translate this job.
*
* @param TMGMTJob $job
* The job entity.
*
* Might be called when canTranslate() returns FALSE to get a reason that
* can be displayed to the user.
*
* @todo Remove this once http://drupal.org/node/1420364 is done.
*/
public function getNotCanTranslateReason(TMGMTJob $job);
/**
* Specifies default mappings for local to remote language codes.
*
* This method can be used in case we know in advance what language codes are
* used by the remote translator and to which local language codes they
* correspond.
*
* @return array
* An array of local => remote language codes.
*
* @ingroup tmgmt_remote_languages_mapping
*/
public function getDefaultRemoteLanguagesMappings();
/**
* Gets all supported languages of the translator.
*
* This list of all language codes used by the remote translator is then used
* for example in the translator settings form to select which remote language
* code correspond to which local language code.
*
* @param TMGMTTranslator $translator
* Translator entity for which to get supported languages.
*
* @return array
* An array of language codes which are provided by the translator
* (remote language codes).
*
* @ingroup tmgmt_remote_languages_mapping
*/
public function getSupportedRemoteLanguages(TMGMTTranslator $translator);
/**
* Gets existing remote languages mappings.
*
* This method is responsible to provide all local to remote language pairs.
*
* @param TMGMTTranslator $translator
* Translator entity for which to get mappings.
*
* @return array
* An array of local => remote language codes.
*
* @ingroup tmgmt_remote_languages_mapping
*/
public function getRemoteLanguagesMappings(TMGMTTranslator $translator);
/**
* Maps local language to remote language.
*
* @param TMGMTTranslator $translator
* Translator entity for which to get remote language.
* @param $language
* Local language code.
*
* @return string
* Remote language code.
*
* @ingroup tmgmt_remote_languages_mapping
*/
public function mapToRemoteLanguage(TMGMTTranslator $translator, $language);
/**
* Maps remote language to local language.
*
* @param TMGMTTranslator $translator
* Translator entity for which to get local language.
* @param $language
* Remote language code.
*
* @return string
* Local language code.
*
* @ingroup tmgmt_remote_languages_mapping
*/
public function mapToLocalLanguage(TMGMTTranslator $translator, $language);
/**
* Returns all available target languages that are supported by this service
* when given a source language.
*
* @param TMGMTTranslator $translator
* The translator entity.
* @param $source_language
* The source language.
*
* @return array
* An array of remote languages in ISO format.
*
* @ingroup tmgmt_remote_languages_mapping
*/
public function getSupportedTargetLanguages(TMGMTTranslator $translator, $source_language);
/**
* Returns supported language pairs.
*
* This info may be used by other plugins to find out what language pairs
* can handle the translator.
*
* @param TMGMTTranslator $translator
* The translator entity.
*
* @return array
* List of language pairs where a pair is an associative array of
* source_language and target_language.
* Example:
* array(
* array('source_language' => 'en-US', 'target_language' => 'de-DE'),
* array('source_language' => 'en-US', 'target_language' => 'de-CH'),
* )
*
* @ingroup tmgmt_remote_languages_mapping
*/
public function getSupportedLanguagePairs(TMGMTTranslator $translator);
/**
* @abstract
*
* Submits the translation request and sends it to the translation provider.
*
* @param TMGMTJob $job
* The job that should be submitted.
*
* @ingroup tmgmt_remote_languages_mapping
*/
public function requestTranslation(TMGMTJob $job);
/**
* Aborts a translation job.
*
* @param TMGMTJob $job
* The job that should have its translation aborted.
*
* @return boolean
* TRUE if the job could be aborted, FALSE otherwise.
*/
public function abortTranslation(TMGMTJob $job);
/**
* Defines default settings.
*
* @return array
* An array of default settings.
*/
public function defaultSettings();
/**
* Returns if the translator has any settings for the passed job.
*/
public function hasCheckoutSettings(TMGMTJob $job);
/**
* Accept a single data item.
*
* @todo Using job item breaks the current convention which uses jobs.
*
* @param $job_item
* The Job item the accepted data item belongs to.
* @param $key
* The key of the accepted data item.
* The key is an array containing the keys of a nested array hierarchy path.
*
* @return
* TRUE if the approving was succesfull, FALSE otherwise.
* In case of an error, it is the responsibility of the translator to
* provide informations about the failure by adding a message to the job
* item.
*/
public function acceptetDataItem(TMGMTJobItem $job_item, array $key);
/**
* Returns the escaped #text of a data item.
*
* @param array $data_item
* A data item with a #text and optional #escape definitions.
*
* @return string
* The text of the data item with translator-specific escape patterns
* applied.
*/
public function escapeText(array $data_item);
/**
* Removes escape patterns from an escaped text.
*
* @param string $text
* The text from which escape patterns should be removed.
*
* @return string
* The unescaped text.
*/
public function unescapeText($text);
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* @file
* Contains the abstract source base plugin class.
*/
/**
* Default controller class for source plugins.
*
* @ingroup tmgmt_source
*/
abstract class TMGMTDefaultSourcePluginController extends TMGMTPluginBase implements TMGMTSourcePluginControllerInterface {
/**
* {@inheritdoc}
*/
public function getLabel(TMGMTJobItem $job_item) {
return t('@plugin item unavailable (@item)', array('@plugin' => $this->pluginInfo['label'], '@item' => $job_item->item_type . ':' . $job_item->item_id));
}
/**
* {@inheritdoc}
*/
public function getUri(TMGMTJobItem $job_item) {
return array(
'path' => '',
'options' => array(),
);
}
/**
* {@inheritdoc}
*/
public function getItemTypes() {
return isset($this->pluginInfo['item types']) ? $this->pluginInfo['item types'] : array();
}
/**
* {@inheritdoc}
*/
public function getItemTypeLabel($type) {
$types = $this->getItemTypes();
if (isset($types[$type])) {
return $types[$type];
}
return '';
}
/**
* {@inheritdoc}
*/
public function getType(TMGMTJobItem $job_item) {
return ucfirst($job_item->item_type);
}
/**
* {@inheritdoc}
*/
public function getExistingLangCodes(TMGMTJobItem $job_item) {
return array();
}
}

View File

@@ -0,0 +1,246 @@
<?php
/**
* @file
* Contains the abstract translator base plugin class.
*/
/**
* Default controller class for service plugins.
*
* @ingroup tmgmt_translator
*/
abstract class TMGMTDefaultTranslatorPluginController extends TMGMTPluginBase implements TMGMTTranslatorPluginControllerInterface {
protected $supportedRemoteLanguages = array();
protected $remoteLanguagesMappings = array();
/**
* Characters that indicate the beginning of an escaped string.
*
* @var string
*/
protected $escapeStart = '';
/**
* Characters that indicate the end of an escaped string.
*
* @var string
*/
protected $escapeEnd = '';
/**
* {@inheritdoc}
*/
public function isAvailable(TMGMTTranslator $translator) {
// Assume that the translation service is always available.
return TRUE;
}
/**
* {@inheritdoc}
*/
public function canTranslate(TMGMTTranslator $translator, TMGMTJob $job) {
// The job is only translatable if the translator is available too.
if ($this->isAvailable($translator) && array_key_exists($job->target_language, $translator->getSupportedTargetLanguages($job->source_language))) {
// We can only translate this job if the target language of the job is in
// one of the supported languages.
return TRUE;
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function abortTranslation(TMGMTJob $job) {
// Assume that we can abort a translation job at any time.
$job->aborted();
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getDefaultRemoteLanguagesMappings() {
return array();
}
/**
* {@inheritdoc}
*/
public function getSupportedRemoteLanguages(TMGMTTranslator $translator) {
return array();
}
/**
* {@inheritdoc}
*/
public function getRemoteLanguagesMappings(TMGMTTranslator $translator) {
if (!empty($this->remoteLanguagesMappings)) {
return $this->remoteLanguagesMappings;
}
foreach (language_list() as $language => $info) {
$this->remoteLanguagesMappings[$language] = $this->mapToRemoteLanguage($translator, $language);
}
return $this->remoteLanguagesMappings;
}
/**
* {@inheritdoc}
*/
public function mapToRemoteLanguage(TMGMTTranslator $translator, $language) {
if (!tmgmt_provide_remote_languages_mappings($translator)) {
return $language;
}
if (!empty($translator->settings['remote_languages_mappings'][$language])) {
return $translator->settings['remote_languages_mappings'][$language];
}
$default_mappings = $this->getDefaultRemoteLanguagesMappings();
if (isset($default_mappings[$language])) {
return $default_mappings[$language];
}
return $language;
}
/**
* {@inheritdoc}
*/
public function mapToLocalLanguage(TMGMTTranslator $translator, $language) {
if (!tmgmt_provide_remote_languages_mappings($translator)) {
return $language;
}
if (isset($translator->settings['remote_languages_mappings']) && is_array($translator->settings['remote_languages_mappings'])) {
$mappings = $translator->settings['remote_languages_mappings'];
}
else {
$mappings = $this->getDefaultRemoteLanguagesMappings();
}
if ($remote_language = array_search($language, $mappings)) {
return $remote_language;
}
return $language;
}
/**
* {@inheritdoc}
*/
public function getSupportedTargetLanguages(TMGMTTranslator $translator, $source_language) {
$languages = entity_metadata_language_list();
unset($languages[LANGUAGE_NONE], $languages[$source_language]);
return drupal_map_assoc(array_keys($languages));
}
/**
* {@inheritdoc}
*
* Default implementation that gets target languages for each remote language.
* This approach is ineffective and therefore it is advised that a plugin
* should provide own implementation.
*/
public function getSupportedLanguagePairs(TMGMTTranslator $translator) {
$language_pairs = array();
foreach ($this->getSupportedRemoteLanguages($translator) as $source_language) {
foreach ($this->getSupportedTargetLanguages($translator, $source_language) as $target_language) {
$language_pairs[] = array('source_language' => $source_language, 'target_language' => $target_language);
}
}
return $language_pairs;
}
/**
* {@inheritdoc}
*/
public function getNotCanTranslateReason(TMGMTJob $job) {
$wrapper = entity_metadata_wrapper('tmgmt_job', $job);
return t('@translator can not translate from @source to @target.', array('@translator' => $job->getTranslator()->label(), '@source' => $wrapper->source_language->label(), '@target' => $wrapper->target_language->label()));
}
/**
* {@inheritdoc}
*/
public function getNotAvailableReason(TMGMTTranslator $translator) {
return t('@translator is not available. Make sure it is properly !configured.', array('@translator' => $this->pluginInfo['label'], '!configured' => l(t('configured'), 'admin/config/regional/tmgmt_translator/manage/' . $translator->name)));
}
/**
* {@inheritdoc}
*/
public function defaultSettings() {
$defaults = array('auto_accept' => FALSE);
// Check if any default settings are defined in the plugin info.
if (isset($this->pluginInfo['default settings'])) {
return array_merge($defaults, $this->pluginInfo['default settings']);
}
return $defaults;
}
/**
* {@inheritdoc}
*/
public function hasCheckoutSettings(TMGMTJob $job) {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function acceptetDataItem(TMGMTJobItem $job_item, array $key) {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function escapeText(array $data_item) {
if (empty($data_item['#escape'])) {
return $data_item['#text'];
}
$text = $data_item['#text'];
$escape = $data_item['#escape'];
// Sort them in reverse order based/ on the position and process them,
// so that positions don't change.
krsort($escape, SORT_NUMERIC);
foreach ($escape as $position => $info) {
$text = substr_replace($text, $this->getEscapedString($info['string']), $position, strlen($info['string']));
}
return $text;
}
/**
* Returns the escaped string.
*
* Defaults to use the escapeStart and escapeEnd properties but can be
* overriden if a non-static replacement pattern is used.
*
* @param string $string
* String that should be escaped.
* @return string
*/
protected function getEscapedString($string) {
return $this->escapeStart . $string . $this->escapeEnd;
}
/**
* {@inheritdoc}
*/
public function unescapeText($text) {
return preg_replace('/' . preg_quote($this->escapeStart, '/') . '(.+)' . preg_quote($this->escapeEnd, '/') . '/U', '$1', $text);
}
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* Interface for source ui controllers.
*
* @ingroup tmgmt_source
*/
interface TMGMTSourceUIControllerInterface extends TMGMTPluginBaseInterface {
/**
* Form callback for the job item review form.
*/
public function reviewForm($form, &$form_state, TMGMTJobItem $item);
/**
* Form callback for the data item element form.
*/
public function reviewDataItemElement($form, &$form_state, $data_item_key, $parent_key, array $data_item, TMGMTJobItem $item);
/**
* Validation callback for the job item review form.
*/
public function reviewFormValidate($form, &$form_state, TMGMTJobItem $item);
/**
* Submit callback for the job item review form.
*/
public function reviewFormSubmit($form, &$form_state, TMGMTJobItem $item);
/**
* {@inheritdoc}
*
* @see tmgmt_ui_menu().
*/
public function hook_menu();
/**
* {@inheritdoc}
*
* @see tmgmt_ui_forms().
*/
public function hook_forms();
/**
* {@inheritdoc}
*
* @see tmgmt_ui_views_default_views().
*/
public function hook_views_default_views();
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* Interface for translator ui controllers.
*
* @ingroup tmgmt_translator
*/
interface TMGMTTranslatorUIControllerInterface extends TMGMTPluginBaseInterface {
/**
* Form callback for the plugin settings form.
*/
public function pluginSettingsForm($form, &$form_state, TMGMTTranslator $translator, $busy = FALSE);
/**
* Form callback for the checkout settings form.
*/
public function checkoutSettingsForm($form, &$form_state, TMGMTJob $job);
/**
* Retrieves information about a translation job.
*
* Services based translators with remote states should place a Poll button
* here to sync the job state.
*
* @param TMGMTJob $job
* The translation job.
*/
public function checkoutInfo(TMGMTJob $job);
/**
* Form callback for the job item review form.
*/
public function reviewForm($form, &$form_state, TMGMTJobItem $item);
/**
* Form callback for the data item element form.
*/
public function reviewDataItemElement($form, &$form_state, $data_item_key, $parent_key, array $data_item, TMGMTJobItem $item);
/**
* Validation callback for the job item review form.
*/
public function reviewFormValidate($form, &$form_state, TMGMTJobItem $item);
/**
* Submit callback for the job item review form.
*/
public function reviewFormSubmit($form, &$form_state, TMGMTJobItem $item);
}

View File

@@ -0,0 +1,111 @@
<?php
/**
* Default ui controller class for source plugin.
*
* @ingroup tmgmt_source
*/
class TMGMTDefaultSourceUIController extends TMGMTPluginBase implements TMGMTSourceUIControllerInterface {
/**
* {@inheritdoc}
*/
public function reviewForm($form, &$form_state, TMGMTJobItem $item) {
return $form;
}
/**
* {@inheritdoc}
*/
public function reviewDataItemElement($form, &$form_state, $data_item_key, $parent_key, array $data_item, TMGMTJobItem $item) {
return $form;
}
/**
* {@inheritdoc}
*/
public function reviewFormValidate($form, &$form_state, TMGMTJobItem $item) {
// Nothing to do here by default.
}
/**
* {@inheritdoc}
*/
public function reviewFormSubmit($form, &$form_state, TMGMTJobItem $item) {
// Nothing to do here by default.
}
/**
* {@inheritdoc}
*/
public function overviewForm($form, &$form_state, $type) {
return $form;
}
/**
* {@inheritdoc}
*/
public function overviewFormValidate($form, &$form_state, $type) {
// Nothing to do here by default.
}
/**
* {@inheritdoc}
*/
public function overviewFormSubmit($form, &$form_state, $type) {
// Nothing to do here by default.
}
/**
* {@inheritdoc}
*/
public function hook_menu() {
$items = array();
if ($types = tmgmt_source_translatable_item_types($this->pluginType)) {
$defaults = array(
'page callback' => 'drupal_get_form',
'access callback' => 'tmgmt_job_access',
'access arguments' => array('create'),
);
if (isset($this->pluginInfo['file'])) {
$defaults['file'] = $this->pluginInfo['file'];
}
if (isset($this->pluginInfo['file path'])) {
$defaults['file path'] = $this->pluginInfo['file path'];
}
foreach ($types as $type => $name) {
$items['admin/tmgmt/sources/' . $this->pluginType . '_' . $type] = $defaults + array(
'title' => check_plain($name),
'page arguments' => array('tmgmt_ui_' . $this->pluginType . '_source_' . $type . '_overview_form', $this->pluginType, $type),
'type' => MENU_LOCAL_TASK,
);
}
}
return $items;
}
/**
* {@inheritdoc}
*/
public function hook_forms() {
$info = array();
if ($types = tmgmt_source_translatable_item_types($this->pluginType)) {
foreach (array_keys($types) as $type) {
$info['tmgmt_ui_' . $this->pluginType . '_source_' . $type . '_overview_form'] = array(
'callback' => 'tmgmt_ui_source_overview_form',
'wrapper_callback' => 'tmgmt_ui_source_overview_form_defaults',
);
}
}
return $info;
}
/**
* {@inheritdoc}
*/
public function hook_views_default_views() {
return array();
}
}

View File

@@ -0,0 +1,128 @@
<?php
/**
* Default ui controller class for translator plugins.
*
* @ingroup tmgmt_translator
*/
class TMGMTDefaultTranslatorUIController extends TMGMTPluginBase implements TMGMTTranslatorUIControllerInterface {
/**
* {@inheritdoc}
*/
public function pluginSettingsForm($form, &$form_state, TMGMTTranslator $translator, $busy = FALSE) {
if (!empty($translator->plugin)) {
$controller = tmgmt_translator_plugin_controller($translator->plugin);
}
// If current translator is configured to provide remote language mapping
// provide the form to configure mappings, unless it does not exists yet.
if (!empty($controller) && tmgmt_provide_remote_languages_mappings($translator)) {
$form['remote_languages_mappings'] = array(
'#tree' => TRUE,
'#type' => 'fieldset',
'#title' => t('Remote languages mappings'),
'#description' => t('Here you can specify mappings of your local language codes to the translator language codes.'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$options = array();
foreach ($controller->getSupportedRemoteLanguages($translator) as $language) {
$options[$language] = $language;
}
ksort($options);
foreach ($controller->getRemoteLanguagesMappings($translator) as $local_language => $remote_language) {
$form['remote_languages_mappings'][$local_language] = array(
'#type' => 'textfield',
'#title' => tmgmt_language_label($local_language) . ' (' . $local_language . ')',
'#default_value' => $remote_language,
'#size' => 6,
);
if (!empty($options)) {
$form['remote_languages_mappings'][$local_language]['#type'] = 'select';
$form['remote_languages_mappings'][$local_language]['#options'] = $options;
$form['remote_languages_mappings'][$local_language]['#empty_option'] = ' - ';
unset($form['remote_languages_mappings'][$local_language]['#size']);
}
}
}
if (!element_children($form)) {
$form['#description'] = t("The @plugin plugin doesn't provide any settings.", array('@plugin' => $this->pluginInfo['label']));
}
return $form;
}
/**
* {@inheritdoc}
*/
public function checkoutSettingsForm($form, &$form_state, TMGMTJob $job) {
if (!element_children($form)) {
$form['#description'] = t("The @translator translator doesn't provide any checkout settings.", array('@translator' => $job->getTranslator()->label()));
}
return $form;
}
/**
* {@inheritdoc}
*/
public function checkoutInfo(TMGMTJob $job) {
return array();
}
/**
* Provides a simple wrapper for the checkout info fieldset.
*
* @param TMGMTJob $job
* Translation job object.
* @param $form
* Partial form structure to be wrapped in the fieldset.
*
* @return
* The provided form structure wrapped in a collapsed fieldset.
*/
public function checkoutInfoWrapper(TMGMTJob $job, $form) {
$label = $job->getTranslator()->label();
$form += array(
'#title' => t('@translator translation job information', array('@translator' => $label)),
'#type' => 'fieldset',
'#collapsible' => TRUE,
);
return $form;
}
/**
* {@inheritdoc}
*/
public function reviewForm($form, &$form_state, TMGMTJobItem $item) {
return $form;
}
/**
* {@inheritdoc}
*/
public function reviewDataItemElement($form, &$form_state, $data_item_key, $parent_key, array $data_item, TMGMTJobItem $item) {
return $form;
}
/**
* {@inheritdoc}
*/
public function reviewFormValidate($form, &$form_state, TMGMTJobItem $item) {
// Nothing to do here by default.
}
/**
* {@inheritdoc}
*/
public function reviewFormSubmit($form, &$form_state, TMGMTJobItem $item) {
// Nothing to do here by default.
}
}

View File

@@ -0,0 +1,8 @@
.tmgmt-entity-sources-wrapper .form-item {
float: left;
margin: 0 10px 0 0;
}
.tmgmt-entity-sources-wrapper #edit-search-submit {
margin: 26px 10px 0 10px;
}

View File

@@ -0,0 +1,12 @@
name = "Entity source plugin tests"
description = "Support module for entity source testing."
package = Testing
core = 7.x
hidden = TRUE
; Information added by Drupal.org packaging script on 2016-09-21
version = "7.x-1.0-rc2+1-dev"
core = "7.x"
project = "tmgmt"
datestamp = "1474446494"

View File

@@ -0,0 +1,20 @@
<?php
/**
* @file
* Tests module for the Entity Source plugin.
*/
/**
* Implements hook_entity_info_alter().
*/
function tmgmt_entity_test_entity_info_alter(&$entity_info) {
if (isset($entity_info['taxonomy_term']) && empty($entity_info['taxonomy_term']['translation'])) {
$entity_info['taxonomy_term']['translation'] = array(
'tmgmt_entity_test_translation' => array(
'base path' => 'taxonomy/term/%taxonomy_term',
'alias' => TRUE,
),
);
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* @file
* Hooks provided by the Entity Translation Management module.
*/
/**
* @addtogroup tmgmt_source
* @{
*/
/**
* Allows to alter $query used to list entities on specific entity type overview
* pages.
*
* @see TMGMTEntityDefaultSourceUIController
*/
function hook_tmgmt_entity_type_list_query_alter(EntityFieldQuery $query) {
$query->entityCondition('type', array('article', 'page'));
}
/**
* @} End of "addtogroup tmgmt_source".
*/

View File

@@ -0,0 +1,27 @@
name = Entity Source
description = Entity source plugin for the Translation Management system.
package = Translation Management
core = 7.x
dependencies[] = tmgmt
dependencies[] = tmgmt_field
dependencies[] = entity
dependencies[] = entity_translation
test_dependencies[] = pathauto
test_dependencies[] = file_entity
test_dependencies[] = entityreference
files[] = tmgmt_entity.source.test
files[] = tmgmt_entity.source.none.test
files[] = tmgmt_entity.pathauto.test
files[] = tmgmt_entity.suggestions.test
files[] = tmgmt_entity.plugin.inc
files[] = tmgmt_entity.ui.inc
; Information added by Drupal.org packaging script on 2016-09-21
version = "7.x-1.0-rc2+1-dev"
core = "7.x"
project = "tmgmt"
datestamp = "1474446494"

View File

@@ -0,0 +1,332 @@
<?php
/**
* @file
* Source plugin for the Translation Management system that handles entities.
*/
/**
* Implements hook_tmgmt_source_plugin_info().
*/
function tmgmt_entity_tmgmt_source_plugin_info() {
$info['entity'] = array(
'label' => t('Entity'),
'description' => t('Source handler for entities.'),
'plugin controller class' => 'TMGMTEntitySourcePluginController',
'item types' => array(),
);
$entity_types = array_filter(variable_get('entity_translation_entity_types', array()));
foreach ($entity_types as $entity_key) {
$entity_info = entity_get_info($entity_key);
$info['entity']['item types'][$entity_key] = $entity_info['label'];
}
return $info;
}
/**
* Implements hook_form_ID_alter().
*
* Alters comment node type select box to filter out comment types that belongs
* to non entity translatable node types.
*/
function tmgmt_entity_form_tmgmt_ui_entity_source_comment_overview_form_alter(&$form, &$form_state) {
if (!isset($form['search_wrapper']['search']['node_type'])) {
return;
}
// Change the select name to "type" as in the query submitted value will be
// passed into node.type condition.
$form['search_wrapper']['search']['type'] = $form['search_wrapper']['search']['node_type'];
unset($form['search_wrapper']['search']['node_type']);
// Set new default value.
$form['search_wrapper']['search']['type']['#default_value'] = isset($_GET['type']) ? $_GET['type'] : NULL;
}
/**
* Helper function to get entity translatable bundles.
*
* Note that for comment entity type it will return the same as for node as
* comment bundles have no use (i.e. in queries).
*
* @param string $entity_type
* Drupal entity type.
*
* @return array
* Array of key => values, where key is type and value its label.
*/
function tmgmt_entity_get_translatable_bundles($entity_type) {
// If given entity type does not have entity translations enabled, no reason
// to continue.
if (!in_array($entity_type, variable_get('entity_translation_entity_types', array()))) {
return array();
}
$entity_info = entity_get_info($entity_type);
$translatable_bundle_types = array();
foreach ($entity_info['bundles'] as $bundle_type => $bundle_definition) {
if ($entity_type == 'comment') {
$bundle_type = str_replace('comment_node_', '', $bundle_type);
if (variable_get('language_content_type_' . $bundle_type) == ENTITY_TRANSLATION_ENABLED) {
$translatable_bundle_types[$bundle_type] = $bundle_definition['label'];
}
}
elseif ($entity_type == 'node') {
if (variable_get('language_content_type_' . $bundle_type) == ENTITY_TRANSLATION_ENABLED) {
$translatable_bundle_types[$bundle_type] = $bundle_definition['label'];
}
}
else {
$translatable_bundle_types[$bundle_type] = $bundle_definition['label'];
}
}
return $translatable_bundle_types;
}
/**
* Gets translatable entities of a given type.
*
* Additionally you can specify entity property conditions, pager and limit.
*
* @param string $entity_type
* Drupal entity type.
* @param array $property_conditions
* Entity properties. There is no value processing so caller must make sure
* the provided entity property exists for given entity type and its value
* is processed.
* @param bool $pager
* Flag to determine if pager will be used.
*
* @return array
* Array of translatable entities.
*/
function tmgmt_entity_get_translatable_entities($entity_type, $property_conditions = array(), $pager = FALSE) {
if (!in_array($entity_type, variable_get('entity_translation_entity_types', array()))) {
return array();
}
$languages = drupal_map_assoc(array_keys(language_list()));
$entity_info = entity_get_info($entity_type);
$label_key = isset($entity_info['entity keys']['label']) ? $entity_info['entity keys']['label'] : NULL;
$id_key = $entity_info['entity keys']['id'];
$query = db_select($entity_info['base table'], 'e');
$query->addTag('tmgmt_entity_get_translatable_entities');
$query->addField('e', $id_key);
// Language neutral entities are not translatable. Filter them out. To do
// that: join {entity_translation} table, but only records with source column
// empty. The {entity_translation}.language will represent the original entity
// language in that case.
$source_table_alias = $query->leftJoin('entity_translation', NULL, "%alias.entity_type = :entity_type AND %alias.entity_id = e.$id_key AND %alias.source = ''", array(':entity_type' => $entity_type));
$query->condition("$source_table_alias.language", LANGUAGE_NONE, '<>');
// Searching for sources with missing translation.
if (!empty($property_conditions['target_status']) && !empty($property_conditions['target_language']) && in_array($property_conditions['target_language'], $languages)) {
$translation_table_alias = db_escape_field('et_' . $property_conditions['target_language']);
$query->leftJoin('entity_translation', $translation_table_alias, "%alias.entity_type = :entity_type AND %alias.entity_id = e.$id_key AND %alias.language = :language",
array(':entity_type' => $entity_type, ':language' => $property_conditions['target_language']));
// Exclude entities with having source language same as the target language
// we search for.
$query->condition('e.language', $property_conditions['target_language'], '<>');
if ($property_conditions['target_status'] == 'untranslated_or_outdated') {
$or = db_or();
$or->isNull("$translation_table_alias.language");
$or->condition("$translation_table_alias.translate", 1);
$query->condition($or);
}
elseif ($property_conditions['target_status'] == 'outdated') {
$query->condition("$translation_table_alias.translate", 1);
}
elseif ($property_conditions['target_status'] == 'untranslated') {
$query->isNull("$translation_table_alias.language");
}
}
// Remove the condition so we do not try to add it again below.
unset($property_conditions['target_language']);
unset($property_conditions['target_status']);
// Searching for the source label.
if (!empty($label_key) && isset($property_conditions[$label_key])) {
$search_tokens = explode(' ', $property_conditions[$label_key]);
$or = db_or();
foreach ($search_tokens as $search_token) {
$search_token = trim($search_token);
if (strlen($search_token) > 2) {
$or->condition($label_key, "%$search_token%", 'LIKE');
}
}
if ($or->count() > 0) {
$query->condition($or);
}
unset($property_conditions[$label_key]);
}
// Searching by taxonomy bundles - we need to switch to vid as the bundle key.
if ($entity_type == 'taxonomy_term' && !empty($property_conditions['vocabulary_machine_name'])) {
$property_name = 'vid';
$vocabulary = taxonomy_vocabulary_machine_name_load($property_conditions['vocabulary_machine_name']);
$property_value = $vocabulary->vid;
$query->condition('e.' . $property_name, $property_value);
// Remove the condition so we do not try to add it again below.
unset($property_conditions['vocabulary_machine_name']);
}
// Searching by the node bundles - that applies for node entities as well as
// comment.
elseif (in_array($entity_type, array('comment', 'node'))) {
$node_table_alias = 'e';
// For comments join node table so that we can filter based on type.
if ($entity_type == 'comment') {
$query->join('node', 'n', 'e.nid = n.nid');
$node_table_alias = 'n';
}
// Get translatable node types and check if it is worth to continue.
$translatable_node_types = array_keys(tmgmt_entity_get_translatable_bundles('node'));
if (empty($translatable_node_types)) {
return array();
}
// If we have type property add condition.
if (isset($property_conditions['type'])) {
$query->condition($node_table_alias . '.type', $property_conditions['type']);
// Remove the condition so we do not try to add it again below.
unset($property_conditions['type']);
}
// If not, query db only for translatable node types.
else {
$query->condition($node_table_alias . '.type', $translatable_node_types);
}
}
// Add remaining query conditions which are expected to be handled in a
// generic way.
foreach ($property_conditions as $property_name => $property_value) {
$query->condition('e.' . $property_name, $property_value);
}
if ($pager) {
$query = $query->extend('PagerDefault')->limit(variable_get('tmgmt_source_list_limit', 20));
}
else {
$query->range(0, variable_get('tmgmt_source_list_limit', 20));
}
$query->orderBy($entity_info['entity keys']['id'], 'DESC');
$entity_ids = $query->execute()->fetchCol();
$entities = array();
if (!empty($entity_ids)) {
$entities = entity_load($entity_type, $entity_ids);
}
return $entities;
}
/**
* Implements hook_tmgmt_source_suggestions()
*/
function tmgmt_entity_tmgmt_source_suggestions(array $items, TMGMTJob $job) {
$suggestions = array();
// Get all translatable entity types.
$entity_types = array_filter(variable_get('entity_translation_entity_types', array()));
foreach ($items as $item) {
if (($item instanceof TMGMTJobItem) && ($item->plugin == 'entity') || ($item->plugin == 'node')) {
// Load the entity and extract the bundle name to get all fields from the
// current entity.
$entity = entity_load_single($item->item_type, $item->item_id);
list(, , $bundle) = entity_extract_ids($item->item_type, $entity);
$field_instances = field_info_instances($item->item_type, $bundle);
// Loop over all fields, check if they are NOT translatable. Only if a
// field is not translatable we may suggest a referenced entity. If so,
// check for a supported field type (image and file currently here).
foreach ($field_instances as $instance) {
$field = field_info_field($instance['field_name']);
$field_type = $field['type'];
$field_name = $field['field_name'];
switch ($field_type) {
case 'file':
case 'image':
// 'File' (and images) must be translatable entity types.
// Other files we not suggest here. Get all field items from the
// current field and suggest them as translatable.
if (isset($entity_types['file']) && ($field_items = field_get_items($item->item_type, $entity, $field_name))) {
// Add all files as a suggestion.
foreach ($field_items as $field_item) {
$file_entity = entity_load_single('file', $field_item['fid']);
// Check if there is already a translation available for this
// file. If so, just continue with the next file.
$handler = entity_translation_get_handler('file', $file_entity);
if ($handler instanceof EntityTranslationHandlerInterface) {
$translations = $handler->getTranslations();
if (isset($translations->data[$job->target_language])) {
continue;
}
}
// Add the translation as a suggestion.
$suggestions[] = array(
'job_item' => tmgmt_job_item_create('entity', 'file', $file_entity->fid),
'reason' => t('Field @label', array('@label' => $instance['label'])),
'from_item' => $item->tjiid,
);
}
}
break;
case 'entityreference':
$target_type = $field['settings']['target_type'];
// Make sure only tranlatable entity types are suggested.
if (isset($entity_types[$target_type]) && ($field_items = field_get_items($item->item_type, $entity, $field_name))) {
// Add all referenced entities as suggestion.
foreach ($field_items as $field_item) {
$ref_entity = entity_load_single($target_type, $field_item['target_id']);
// Check if there is already a translation available for this
// entity. If so, just continue with the next one.
$handler = entity_translation_get_handler($target_type, $ref_entity);
if ($handler instanceof EntityTranslationHandlerInterface) {
$translations = $handler->getTranslations();
if (isset($translations->data[$job->target_language])) {
continue;
}
}
// Add suggestion.
$suggestions[] = array(
'job_item' => tmgmt_job_item_create('entity', $target_type, $field_item['target_id']),
'reason' => t('Field @label', array('@label' => $instance['label'])),
'from_item' => $item->tjiid,
);
}
}
break;
}
}
}
}
return $suggestions;
}

View File

@@ -0,0 +1,55 @@
<?php
/**
* Tests integration with pathauto.
*/
class TMGMTEntitySourcePathAutoTestCase extends TMGMTEntityTestCaseUtility {
static function getInfo() {
return array(
'name' => 'Entity Source Pathauto tests',
'description' => 'Verifies that the correct aliases are generated for entity transations',
'group' => 'Translation Management',
'dependencies' => array('entity_translation', 'pathauto'),
);
}
function setUp() {
parent::setUp(array('tmgmt_entity', 'entity_translation', 'pathauto'));
$this->loginAsAdmin();
$this->createNodeType('article', 'Article', ENTITY_TRANSLATION_ENABLED);
}
/**
* Tests that pathauto aliases are correctly created.
*/
function testAliasCreation() {
$this->setEnvironment('de');
// Create a translation job.
$job = $this->createJob();
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
// Create a node.
$node = $this->createNode('article');
// Create a job item for this node and add it to the job.
$job->addItem('entity', 'node', $node->nid);
// Translate the job.
$job->requestTranslation();
// Check the translated job items.
foreach ($job->getItems() as $item) {
$item->acceptTranslation();
}
// Make sure that the correct url aliases were created.
$aliases = db_query('SELECT * FROM {url_alias} where source = :source', array(':source' => 'node/' . $node->nid))->fetchAllAssoc('language');
$this->assertEqual(2, count($aliases));
$this->assertTrue(isset($aliases['en']), 'English alias created.');
$this->assertTrue(isset($aliases['de']), 'German alias created.');
}
}

View File

@@ -0,0 +1,110 @@
<?php
/**
* @file
* Provides the Entity source controller.
*/
class TMGMTEntitySourcePluginController extends TMGMTDefaultSourcePluginController {
public function getLabel(TMGMTJobItem $job_item) {
if ($entity = entity_load_single($job_item->item_type, $job_item->item_id)) {
return entity_label($job_item->item_type, $entity);
}
}
public function getUri(TMGMTJobItem $job_item) {
if ($entity = entity_load_single($job_item->item_type, $job_item->item_id)) {
return entity_uri($job_item->item_type, $entity);
}
}
/**
* {@inheritdoc}
*
* Returns the data from the fields as a structure that can be processed by
* the Translation Management system.
*/
public function getData(TMGMTJobItem $job_item) {
$entity = entity_load_single($job_item->item_type, $job_item->item_id);
if (!$entity) {
throw new TMGMTException(t('Unable to load entity %type with id %id', array('%type' => $job_item->item_type, $job_item->item_id)));
}
if (entity_language($job_item->item_type, $entity) == LANGUAGE_NONE) {
throw new TMGMTException(t('Entity %entity could not be translated because it is language neutral', array('%entity' => entity_label($job_item->item_type, $entity))));
}
return tmgmt_field_get_source_data($job_item->item_type, $entity, $job_item->getJob()->source_language, TRUE);
}
/**
* {@inheritdoc}
*/
public function saveTranslation(TMGMTJobItem $job_item) {
$entity = entity_load_single($job_item->item_type, $job_item->item_id);
$job = tmgmt_job_load($job_item->tjid);
tmgmt_field_populate_entity($job_item->item_type, $entity, $job->target_language, $job_item->getData());
// Change the active language of the entity to the target language.
$handler = entity_translation_get_handler($job_item->item_type, $entity);
$handler->setFormLanguage($job_item->getJob()->target_language);
entity_save($job_item->item_type, $entity);
$translation = array(
// @todo Improve hardcoded values.
'translate' => 0,
'status' => TRUE,
'language' => $job_item->getJob()->target_language,
'source' => $job_item->getJob()->source_language,
);
$handler->setTranslation($translation);
$handler->saveTranslations();
$job_item->accepted();
}
/**
* {@inheritdoc}
*/
public function getType(TMGMTJobItem $job_item) {
if ($entity = entity_load_single($job_item->item_type, $job_item->item_id)) {
$bundles = tmgmt_entity_get_translatable_bundles($job_item->item_type);
$info = entity_get_info($job_item->item_type);
list(, , $bundle) = entity_extract_ids($job_item->item_type, $entity);
// Display entity type and label if we have one and the bundle isn't
// the same as the entity type.
if (isset($bundles[$bundle]) && $bundle != $job_item->item_type) {
return t('@type (@bundle)', array('@type' => $info['label'], '@bundle' => $bundles[$bundle]));
}
// Otherwise just display the entity type label.
elseif (isset($info['label'])) {
return $info['label'];
}
return parent::getType($job_item);
}
}
/**
* {@inheritdoc}
*/
public function getSourceLangCode(TMGMTJobItem $job_item) {
$entity = entity_load_single($job_item->item_type, $job_item->item_id);
return isset($entity->translations->original) ? $entity->translations->original : NULL;
}
/**
* {@inheritdoc}
*/
public function getExistingLangCodes(TMGMTJobItem $job_item) {
if ($entity = entity_load_single($job_item->item_type, $job_item->item_id)) {
$entity_info = entity_get_info($job_item->item_type);
if (isset($entity_info['entity keys']['translations'])){
$translations_key = $entity_info['entity keys']['translations'];
return array_keys($entity->{$translations_key}->data);
}
}
return array();
}
}

View File

@@ -0,0 +1,101 @@
<?php
/**
* Entity source LANGUAGE_NONE tests.
*
* Splitted into a separate test because of https://www.drupal.org/node/2675230.
*/
class TMGMTEntitySourceLanguageNoneTestCase extends TMGMTEntityTestCaseUtility {
public $vocabulary;
static function getInfo() {
return array(
'name' => 'Entity Source Neutral tests',
'description' => 'Tests that LANGUAGE_NONE entities can not be translated',
'group' => 'Translation Management',
'dependencies' => array('entity_translation'),
);
}
function setUp() {
parent::setUp(array('tmgmt_entity', 'taxonomy', 'entity_translation'));
// Admin user to perform settings on setup.
$this->loginAsAdmin(array('administer entity translation'));
$this->vocabulary = $this->createTaxonomyVocab(strtolower($this->randomName()), $this->randomName(), array(FALSE, TRUE, TRUE, TRUE));
// Enable entity translations for taxonomy.
$edit['entity_translation_entity_types[taxonomy_term]'] = 1;
$this->drupalPost('admin/config/regional/entity_translation', $edit, t('Save configuration'));
}
/**
* Test if language neutral entities are not allowed for translation.
*
* That behaviour is described in the entity_translation documentation:
* https://www.drupal.org/node/1280934
*/
function testLanguageNeutral() {
$this->setEnvironment('de');
// Structure: array({entity-type} => array({source-langcode} => {entity}))
$test_data = array();
$this->createNodeType('article', 'Article', ENTITY_TRANSLATION_ENABLED);
$test_data['node'][LANGUAGE_NONE] = $this->createNode('article', LANGUAGE_NONE);
$test_data['node']['en'] = $this->createNode('article', 'en');
$test_data['node']['de'] = $this->createNode('article', 'de');
$test_data['taxonomy_term'][LANGUAGE_NONE] = $this->createTaxonomyTerm($this->vocabulary, LANGUAGE_NONE);
$test_data['taxonomy_term']['en'] = $this->createTaxonomyTerm($this->vocabulary, 'en');
$test_data['taxonomy_term']['de'] = $this->createTaxonomyTerm($this->vocabulary, 'de');
// Test if tmgmt_entity_get_translatable_entities() function excludes
// language neutral entities.
foreach ($test_data as $entity_type => $entities) {
$translatable_entities = tmgmt_entity_get_translatable_entities($entity_type);
foreach ($entities as $langcode => $entity) {
list($id, , ) = entity_extract_ids($entity_type, $entity);
if ($langcode == LANGUAGE_NONE) {
$this->assert(!isset($translatable_entities[$id]), "Language neutral $entity_type entity does not exist in the translatable entities list.");
}
else {
$this->assert(isset($translatable_entities[$id]), "$langcode $entity_type entity exists in the translatable entities list.");
}
}
}
// Test if language neutral entities can't be added to a translation job.
$job = $this->createJob();
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
foreach ($test_data as $entity_type => $entities) {
foreach ($entities as $langcode => $entity) {
list($id, , ) = entity_extract_ids($entity_type, $entity);
try {
$job->addItem('entity', $entity_type, $id);
if ($langcode == LANGUAGE_NONE) {
$this->fail("Adding of language neutral $entity_type entity to a translation job did not fail.");
}
else {
$this->pass("Adding of $langcode $entity_type entity node to a translation job did not fail.");
}
}
catch (TMGMTException $e) {
if ($langcode == LANGUAGE_NONE) {
$this->pass("Adding of language neutral $entity_type entity to a translation job did fail.");
}
else {
$this->fail("Adding of $langcode $entity_type entity node to a translation job did fail.");
}
}
}
}
$GLOBALS['TMGMT_DEBUG'] = FALSE;
}
}

View File

@@ -0,0 +1,212 @@
<?php
/**
* Basic Entity Source tests.
*/
class TMGMTEntitySourceTestCase extends TMGMTEntityTestCaseUtility {
public $vocabulary;
static function getInfo() {
return array(
'name' => 'Entity Source tests',
'description' => 'Exporting source data from entities and saving translations back to entities.',
'group' => 'Translation Management',
'dependencies' => array('entity_translation'),
);
}
function setUp() {
parent::setUp(array('tmgmt_entity', 'taxonomy', 'entity_translation'));
// Admin user to perform settings on setup.
$this->loginAsAdmin(array('administer entity translation'));
$this->vocabulary = $this->createTaxonomyVocab(strtolower($this->randomName()), $this->randomName(), array(FALSE, TRUE, TRUE, TRUE));
// Enable entity translations for taxonomy.
$edit['entity_translation_entity_types[taxonomy_term]'] = 1;
$this->drupalPost('admin/config/regional/entity_translation', $edit, t('Save configuration'));
}
/**
* Tests nodes field translation.
*/
function testEntitySourceNode() {
$this->setEnvironment('de');
$this->createNodeType('article', 'Article', ENTITY_TRANSLATION_ENABLED);
// Create a translation job.
$job = $this->createJob();
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
// Create some nodes.
for ($i = 1; $i <= 5; $i++) {
$node = $this->createNode('article');
// Create a job item for this node and add it to the job.
$item = $job->addItem('entity', 'node', $node->nid);
$this->assertEqual(t('@type (@bundle)', array('@type' => t('Node'), '@bundle' => 'Article')), $item->getSourceType());
}
// Translate the job.
$job->requestTranslation();
// Check the translated job items.
foreach ($job->getItems() as $item) {
// The source is available only for en.
$this->assertJobItemLangCodes($item, 'en', array('en'));
$item->acceptTranslation();
$this->assertTrue($item->isAccepted());
$entity = entity_load_single($item->item_type, $item->item_id);
$data = $item->getData();
$this->checkTranslatedData($entity, $data, 'de');
$this->checkUntranslatedData($entity, $this->field_names['node']['article'], $data, 'de');
// The source is now available for both en and de.
$this->assertJobItemLangCodes($item, 'en', array('de', 'en'));
}
}
/**
* Tests taxonomy terms field translation.
*/
function testEntitySourceTerm() {
$this->setEnvironment('de');
// Create the job.
$job = $this->createJob();
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
$term = NULL;
//Create some terms.
for ($i = 1; $i <= 5; $i++) {
$term = $this->createTaxonomyTerm($this->vocabulary);
// Create the item and assign it to the job.
$item = $job->addItem('entity', 'taxonomy_term', $term->tid);
$this->assertEqual(t('@type (@bundle)', array('@type' => t('Taxonomy term'), '@bundle' => $this->vocabulary->name)), $item->getSourceType());
}
// Request the translation and accept it.
$job->requestTranslation();
// Check if the fields were translated.
foreach ($job->getItems() as $item) {
$this->assertJobItemLangCodes($item, 'en', array('en'));
$item->acceptTranslation();
$entity = entity_load_single($item->item_type, $item->item_id);
$data = $item->getData();
$this->checkTranslatedData($entity, $data, 'de');
$this->checkUntranslatedData($entity, $this->field_names['taxonomy_term'][$this->vocabulary->machine_name], $data, 'de');
$this->assertJobItemLangCodes($item, 'en', array('de', 'en'));
}
}
function testAddingJobItemsWithEmptySourceText() {
$this->setEnvironment('de');
// Create term with empty texts.
$empty_term = new stdClass();
$empty_term->name = $this->randomName();
$empty_term->description = $this->randomName();
$empty_term->vid = $this->vocabulary->vid;
taxonomy_term_save($empty_term);
// Create the job.
$job = tmgmt_job_create('en', NULL);
try {
$job->addItem('entity', 'taxonomy_term', $empty_term->tid);
$this->fail('Job item added with empty source text.');
}
catch (TMGMTException $e) {
$this->assert(empty($job->tjid), 'After adding a job item with empty source text its tjid has to be unset.');
}
// Create term with populated source content.
$populated_content_term = $this->createTaxonomyTerm($this->vocabulary);
// Lets reuse the last created term with populated source content.
$job->addItem('entity', 'taxonomy_term', $populated_content_term->tid);
$this->assert(!empty($job->tjid), 'After adding another job item with populated source text its tjid must be set.');
}
/**
* Test if the source is able to pull content in requested language.
*/
function testRequestDataForSpecificLanguage() {
$this->setEnvironment('de');
$this->setEnvironment('cs');
$this->createNodeType('article', 'Article', ENTITY_TRANSLATION_ENABLED);
// Create a translation job.
$job = $this->createJob('en', 'de');
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
$node = $this->createNode('article', 'cs');
$node->body['en'][0]['value'] = 'en translation';
node_save($node);
$job->addItem('entity', 'node', $node->nid);
$data = $job->getData();
$this->assertEqual($data[1]['body'][0]['value']['#text'], 'en translation');
}
/**
* Compares the data from an entity with the translated data.
*
* @param $tentity
* The translated entity object.
* @param $data
* An array with the translated data.
* @param $langcode
* The code of the target language.
*/
function checkTranslatedData($tentity, $data, $langcode) {
foreach (element_children($data) as $field_name) {
foreach (element_children($data[$field_name]) as $delta) {
foreach (element_children($data[$field_name][$delta]) as $column) {
$column_value = $data[$field_name][$delta][$column];
if (!empty($column_value['#translate'])) {
$this->assertEqual($tentity->{$field_name}[$langcode][$delta][$column], $column_value['#translation']['#text'], format_string('The field %field:%delta has been populated with the proper translated data.', array('%field' => $field_name, 'delta' => $delta)));
}
else {
$this->assertEqual($tentity->{$field_name}[$langcode][$delta][$column], $column_value['#text'], format_string('The field %field:%delta has been populated with the proper untranslated data.', array('%field' => $field_name, 'delta' => $delta)));
}
}
}
}
}
/**
* Checks the fields that should not be translated.
*
* @param $tentity
* The translated entity object.
* @param $fields
* An array with the field names to check.
* @param $translation
* An array with the translated data.
* @param $langcode
* The code of the target language.
*/
function checkUntranslatedData($tentity, $fields, $data, $langcode) {
foreach ($fields as $field_name) {
$field_info = field_info_field($field_name);
if (!$field_info['translatable']) {
// Avoid some PHP warnings.
if (isset($data[$field_name])) {
$this->assertNull($data[$field_name]['#translation']['#text'], 'The not translatable field was not translated.');
}
if (isset($tentity->{$field_name}[$langcode])) {
$this->assertNull($tentity->{$field_name}[$langcode], 'The entity has translated data in a field that is translatable.');
}
}
}
}
}

View File

@@ -0,0 +1,274 @@
<?php
/*
* @file
* Contains tests for Translation management
*/
/**
* Basic Source-Suggestions tests.
*/
class TMGMTSuggestionsTestCase extends TMGMTBaseTestCase {
static function getInfo() {
return array(
'name' => 'Entity Suggestions tests',
'description' => 'Tests suggestion implementation for the entity source plugin',
'group' => 'Translation Management',
'dependencies' => array(
'file_entity',
'entityreference',
),
);
}
public function setUp() {
parent::setUp(array('file_entity', 'tmgmt_entity', 'tmgmt_ui', 'entityreference', 'tmgmt_i18n_string', 'i18n_menu'));
$this->loginAsAdmin(array('administer entity translation'));
$this->setEnvironment('de');
// Enable entity translations for nodes and comments.
$edit = array();
$edit['entity_translation_entity_types[node]'] = 1;
$edit['entity_translation_entity_types[file]'] = 1;
$this->drupalPost('admin/config/regional/entity_translation', $edit, t('Save configuration'));
}
/**
* Prepare a node to get suggestions from.
*
* Creates a node with two file fields. The first one is not translatable,
* the second one is. Both fields got two files attached, where one has
* translatable content (title and atl-text) and the other one not.
*
* @return object
* The node which is prepared with all needed fields for the suggestions.
*/
protected function prepareTranslationSuggestions() {
// Create a content type with fields.
// Only the first field is a translatable reference.
$type = $this->drupalCreateContentType();
$field1 = field_create_field(array(
'field_name' => 'field1',
'type' => 'file',
'cardinality' => -1,
));
$field2 = field_create_field(array(
'field_name' => 'field2',
'type' => 'file',
'cardinality' => -1,
'translatable' => TRUE,
));
$field3 = field_create_field(array(
'field_name' => 'field3',
'type' => 'entityreference',
'cardinality' => -1,
'settings' => array(
'target_type' => 'node',
'handler' => 'base',
'handler_settings' => array(
'target_bundles' => array($type->type => $type->type),
'sort' => array('type' => 'none'),
),
),
));
// Create field instances on the content type.
field_create_instance(array(
'field_name' => $field1['field_name'],
'entity_type' => 'node',
'bundle' => $type->type,
'label' => 'Field 1',
'widget' => array('type' => 'file'),
'settings' => array(),
));
field_create_instance(array(
'field_name' => $field2['field_name'],
'entity_type' => 'node',
'bundle' => $type->type,
'label' => 'Field 2',
'widget' => array('type' => 'file'),
'settings' => array(),
));
field_create_instance(array(
'field_name' => $field3['field_name'],
'entity_type' => 'node',
'bundle' => $type->type,
'label' => 'Field 3',
'settings' => array(),
'widget' => array('type' => 'entityreference_autocomplete_tags'),
));
// Make the body field translatable from node.
$info = field_info_field('body');
$info['translatable'] = TRUE;
field_update_field($info);
// Make the file entity fields translatable.
$info = field_info_field('field_file_image_alt_text');
$info['translatable'] = TRUE;
field_update_field($info);
$info = field_info_field('field_file_image_title_text');
$info['translatable'] = TRUE;
field_update_field($info);
// Create and save files - two with some text and two with no text.
list($file1, $file2, $file3, $file4) = $this->drupalGetTestFiles('image');
$file2->field_file_image_alt_text['en'][0] = array(
'value' => $this->randomName(),
'type' => 'plain_text',
);
$file2->field_file_image_title_text['en'][0] = array(
'value' => $this->randomName() . ' ' . $this->randomName(),
'type' => 'plain_text',
);
$file4->field_file_image_alt_text['en'][0] = array(
'value' => $this->randomName(),
'type' => 'plain_text',
);
$file4->field_file_image_title_text['en'][0] = array(
'value' => $this->randomName() . ' ' . $this->randomName(),
'type' => 'plain_text',
);
file_save($file1);
file_save($file2);
file_save($file3);
file_save($file4);
// Create a dummy node that will be referenced
$referenced_node = $this->drupalCreateNode(array(
'type' => $type->type,
'language' => 'en',
'body' => array(
'en' => array(
array('value' => $this->randomName() . ' ' . $this->randomName()),
),
),
));
// Create a node with two translatable and two non-translatable files.
$node = $this->drupalCreateNode(array(
'type' => $type->type,
'language' => 'en',
'body' => array('en' => array(
array(
'value' => $this->randomName(),
),
)),
$field1['field_name'] => array(LANGUAGE_NONE => array(
array(
'fid' => $file1->fid,
'display' => 1,
'description' => '',
),
array(
'fid' => $file2->fid,
'display' => 1,
'description' => '',
),
)),
$field2['field_name'] => array(LANGUAGE_NONE => array(
array(
'fid' => $file3->fid,
'display' => 1,
'description' => '',
),
array(
'fid' => $file4->fid,
'display' => 1,
'description' => '',
),
)),
$field3['field_name'] => array(LANGUAGE_NONE => array(
array('target_id' => $referenced_node->nid),
)),
));
// Create a translatable menu.
$config = array(
'menu_name' => 'translatable-menu',
'title' => 'Translatable menu',
'description' => $this->randomName(),
'i18n_mode' => I18N_MODE_MULTIPLE,
);
menu_save($config);
$menu = menu_load($config['menu_name']);
// Create a menu link for the node.
$menu_link = array(
'link_path' => 'node/' . $node->nid,
'link_title' => 'Menu link one',
// i18n_menu_link::get_title() uses the title, set that too.
'title' => 'Menu link one',
'menu_name' => $menu['menu_name'],
'customized' => TRUE,
);
$node->link = menu_link_load(menu_link_save($menu_link));
return $node;
}
/**
* Test suggested entities from a translation job.
*/
public function testSuggestions() {
// Prepare a job and a node for testing.
$job = $this->createJob();
$node = $this->prepareTranslationSuggestions();
$item = $job->addItem('entity', 'node', $node->nid);
// Get all suggestions and clean the list.
$suggestions = $job->getSuggestions();
$job->cleanSuggestionsList($suggestions);
// Check for suggestions.
$this->assertEqual(count($suggestions), 4, 'Found four suggestions.');
// Check for valid attributes on the suggestions.
foreach ($suggestions as $suggestion) {
switch ($suggestion['reason']) {
case 'Field Field 1':
$this->assertEqual($suggestion['job_item']->getWordCount(), 3, 'Three translatable words in the suggestion.');
$this->assertEqual($suggestion['job_item']->plugin, 'entity', 'Got an entity as plugin in the suggestion.');
$this->assertEqual($suggestion['job_item']->item_type, 'file', 'Got a file in the suggestion.');
$this->assertEqual($suggestion['job_item']->item_id, $node->field1[LANGUAGE_NONE][1]['fid'], 'File id match between node and suggestion.');
break;
case 'Field Field 2':
$this->assertEqual($suggestion['job_item']->getWordCount(), 3, 'Three translatable words in the suggestion.');
$this->assertEqual($suggestion['job_item']->plugin, 'entity', 'Got an entity as plugin in the suggestion.');
$this->assertEqual($suggestion['job_item']->item_type, 'file', 'Got a file in the suggestion.');
$this->assertEqual($suggestion['job_item']->item_id, $node->field2[LANGUAGE_NONE][1]['fid'], 'File id match between node and suggestion.');
break;
case 'Field Field 3':
$this->assertEqual($suggestion['job_item']->getWordCount(), 2, 'Two translatable words in the suggestion');
$this->assertEqual($suggestion['job_item']->plugin, 'entity', 'Got an entity as plugin in the suggestion.');
$this->assertEqual($suggestion['job_item']->item_type, 'node', 'Got a node in the suggestion.');
$this->assertEqual($suggestion['job_item']->item_id, $node->field3[LANGUAGE_NONE][0]['target_id'], 'File id match between node and suggestion.');
break;
case 'Menu link Menu link one':
$this->assertEqual($suggestion['job_item']->getWordCount(), 3, 'Three translatable words in the suggestion' . $suggestion['job_item']->plugin . $suggestion['job_item']->item_type);
$this->assertEqual($suggestion['job_item']->plugin, 'i18n_string', 'Got a string as plugin in the suggestion.');
$this->assertEqual($suggestion['job_item']->item_type, 'menu_link', 'Got a menu link in the suggestion.');
$this->assertEqual($suggestion['job_item']->item_id, 'menu:item:'. $node->link['mlid'], 'Menu link id match between menu link and suggestion.');
break;
default:
$this->fail('Found an invalid suggestion.');
break;
}
$this->assertEqual($suggestion['from_item'], $item->tjiid);
$job->addExistingItem($suggestion['job_item']);
}
// Re-get all suggestions.
$suggestions = $job->getSuggestions();
$job->cleanSuggestionsList($suggestions);
// Check for no more suggestions.
$this->assertEqual(count($suggestions), 0, 'Found no more suggestion.');
}
}

View File

@@ -0,0 +1,257 @@
<?php
/**
* Abstract entity ui controller class for source plugin that provides
* getEntity() method to retrieve list of entities of specific type. It also
* allows to implement alter hook to alter the entity query for a specific type.
*
* @ingroup tmgmt_source
*/
abstract class TMGMTEntityDefaultSourceUIController extends TMGMTDefaultSourceUIController {
/**
* Entity source list items limit.
*
* @var int
*/
public $pagerLimit = 25;
/**
* Gets entities data of provided type needed to build overview form list.
*
* @param $type
* Entity type for which to get list of entities.
* @param array $property_conditions
* Array of key => $value pairs passed into
* tmgmt_entity_get_translatable_entities() as the second parameter.
*
* @return array
* Array of entities.
*/
public function getEntitiesTranslationData($type, $property_conditions = array()) {
$return_value = array();
$entities = tmgmt_entity_get_translatable_entities($type, $property_conditions, TRUE);
$entity_info = entity_get_info($type);
$bundles = tmgmt_entity_get_translatable_bundles($type);
// For retrieved entities add translation specific data.
foreach ($entities as $entity) {
list($entity_id, , $bundle) = entity_extract_ids($type, $entity);
$entity_uri = entity_uri($type, $entity);
// This occurs on user entity type.
if (empty($entity_id)) {
continue;
}
/**
* @var EntityTranslationDefaultHandler $handler
*/
$handler = entity_translation_get_handler($type, $entity);
// Get existing translations and current job items for the entity
// to determine translation statuses
$translations = $handler->getTranslations();
$source_lang = entity_language($type, $entity);
$current_job_items = tmgmt_job_item_load_latest('entity', $type, $entity_id, $source_lang);
// Load basic entity data.
$return_value[$entity_id] = array(
'entity_type' => $type,
'entity_id' => $entity_id,
'entity_label' => entity_label($type, $entity),
'entity_uri' => $entity_uri['path'],
);
if (count($bundles) > 1) {
$return_value[$entity_id]['bundle'] = isset($bundles[$bundle]) ? $bundles[$bundle] : t('Unknown');
}
// Load entity translation specific data.
foreach (language_list() as $langcode => $language) {
$translation_status = 'current';
if ($langcode == $source_lang) {
$translation_status = 'original';
}
elseif (!isset($translations->data[$langcode])) {
$translation_status = 'missing';
}
elseif (!empty($translations->data[$langcode]['translate'])) {
$translation_status = 'outofdate';
}
$return_value[$entity_id]['current_job_items'][$langcode] = isset($current_job_items[$langcode]) ? $current_job_items[$langcode]: NULL;
$return_value[$entity_id]['translation_statuses'][$langcode] = $translation_status;
}
}
return $return_value;
}
/**
* Builds search form for entity sources overview.
*
* @param array $form
* Drupal form array.
* @param $form_state
* Drupal form_state array.
* @param $type
* Entity type.
*
* @return array
* Drupal form array.
*/
public function overviewSearchFormPart($form, &$form_state, $type) {
// Add search form specific styling.
drupal_add_css(drupal_get_path('module', 'tmgmt_entity') . '/css/tmgmt_entity.admin.entity_source_search_form.css');
$form = array();
// Add entity type value into form array so that it is available in
// the form alter hook.
$form_state['entity_type'] = $type;
$form['search_wrapper'] = array(
'#prefix' => '<div class="tmgmt-sources-wrapper tmgmt-entity-sources-wrapper">',
'#suffix' => '</div>',
'#weight' => -15,
);
$form['search_wrapper']['search'] = array(
'#tree' => TRUE,
);
$form['search_wrapper']['search_submit'] = array(
'#type' => 'submit',
'#value' => t('Search'),
'#weight' => 10,
);
$form['search_wrapper']['search_cancel'] = array(
'#type' => 'submit',
'#value' => t('Cancel'),
'#weight' => 11,
);
$entity_info = entity_get_info($type);
$label_key = isset($entity_info['entity keys']['label']) ? $entity_info['entity keys']['label'] : NULL;
if (!empty($label_key)) {
$form['search_wrapper']['search'][$label_key] = array(
'#type' => 'textfield',
'#title' => t('@entity_name title', array('@entity_name' => $entity_info['label'])),
'#size' => 25,
'#default_value' => isset($_GET[$label_key]) ? $_GET[$label_key] : NULL,
);
}
$language_options = array();
foreach (language_list() as $langcode => $language) {
$language_options[$langcode] = $language->name;
}
$form['search_wrapper']['search']['language'] = array(
'#type' => 'select',
'#title' => t('Source Language'),
'#options' => $language_options,
'#empty_option' => t('All'),
'#default_value' => isset($_GET['language']) ? $_GET['language'] : NULL,
);
$bundle_key = $entity_info['entity keys']['bundle'];
$bundle_options = tmgmt_entity_get_translatable_bundles($type);
if (count($bundle_options) > 1) {
$form['search_wrapper']['search'][$bundle_key] = array(
'#type' => 'select',
'#title' => t('@entity_name type', array('@entity_name' => $entity_info['label'])),
'#options' => $bundle_options,
'#empty_option' => t('All'),
'#default_value' => isset($_GET[$bundle_key]) ? $_GET[$bundle_key] : NULL,
);
}
// In case entity translation is not enabled for any of bundles
// display appropriate message.
elseif (count($bundle_options) == 0) {
drupal_set_message(t('Entity translation is not enabled for any of existing content types. To use this functionality go to Content types administration and enable entity translation for desired content types.'), 'warning');
unset($form['search_wrapper']);
}
$options = array();
foreach (language_list() as $langcode => $language) {
$options[$langcode] = $language->name;
}
$form['search_wrapper']['search']['target_language'] = array(
'#type' => 'select',
'#title' => t('Target language'),
'#options' => $options,
'#empty_option' => t('Any'),
'#default_value' => isset($_GET['target_language']) ? $_GET['target_language'] : NULL,
);
$form['search_wrapper']['search']['target_status'] = array(
'#type' => 'select',
'#title' => t('Target status'),
'#options' => array(
'untranslated_or_outdated' => t('Untranslated or outdated'),
'untranslated' => t('Untranslated'),
'outdated' => t('Outdated'),
),
'#default_value' => isset($_GET['target_status']) ? $_GET['target_status'] : NULL,
'#states' => array(
'invisible' => array(
':input[name="search[target_language]"]' => array('value' => ''),
),
),
);
return $form;
}
/**
* Performs redirect with search params appended to the uri.
*
* In case of triggering element is edit-search-submit it redirects to
* current location with added query string containing submitted search form
* values.
*
* @param array $form
* Drupal form array.
* @param $form_state
* Drupal form_state array.
* @param $type
* Entity type.
*/
public function overviewSearchFormRedirect($form, &$form_state, $type) {
if ($form_state['triggering_element']['#id'] == 'edit-search-cancel') {
drupal_goto($_GET['q']);
}
elseif ($form_state['triggering_element']['#id'] == 'edit-search-submit') {
$query = array();
foreach ($form_state['values']['search'] as $key => $value) {
$query[$key] = $value;
}
drupal_goto($_GET['q'], array('query' => $query));
}
}
/**
* {@inheritdoc}
*/
public function hook_menu() {
$items = parent::hook_menu();
if (isset($items['admin/tmgmt/sources/entity_node'])) {
// We assume that nodes are the most important overview if enabled, so
// make sure they show up first.
$items['admin/tmgmt/sources/entity_node']['weight'] = -20;
}
return $items;
}
}

View File

@@ -0,0 +1,20 @@
name = Entity Source User Interface
description = User Interface for the entity translation source plugin.
package = Translation Management
core = 7.x
dependencies[] = tmgmt_entity
dependencies[] = tmgmt_ui
dependencies[] = views_bulk_operations
files[] = tmgmt_entity_ui.test
files[] = tmgmt_entity_ui.list.test
files[] = tmgmt_entity_ui.ui.inc
; Information added by Drupal.org packaging script on 2016-09-21
version = "7.x-1.0-rc2+1-dev"
core = "7.x"
project = "tmgmt"
datestamp = "1474446494"

View File

@@ -0,0 +1,268 @@
<?php
class TMGMTEntitySourceListTestCase extends TMGMTEntityTestCaseUtility {
protected $nodes = array();
static function getInfo() {
return array(
'name' => 'Entity Source List tests',
'description' => 'Tests the user interface for entity translation lists.',
'group' => 'Translation Management',
);
}
function setUp() {
parent::setUp(array('tmgmt_entity_ui', 'translation', 'comment', 'taxonomy'));
$this->loginAsAdmin(array('administer entity translation'));
$this->setEnvironment('de');
$this->setEnvironment('fr');
// Enable entity translations for nodes and comments.
$edit = array();
$edit['entity_translation_entity_types[comment]'] = 1;
$edit['entity_translation_entity_types[node]'] = 1;
$edit['entity_translation_entity_types[taxonomy_term]'] = 1;
$this->drupalPost('admin/config/regional/entity_translation', $edit, t('Save configuration'));
$this->createNodeType('article', 'Article', ENTITY_TRANSLATION_ENABLED);
$this->createNodeType('page', 'Page', TRANSLATION_ENABLED);
// Create nodes that will be used during tests.
// NOTE that the order matters as results are read by xpath based on
// position in the list.
$this->nodes['page']['en'][] = $this->createNode('page');
$this->nodes['article']['de'][0] = $this->createNode('article', 'de');
$this->nodes['article']['fr'][0] = $this->createNode('article', 'fr');
$this->nodes['article']['en'][3] = $this->createNode('article', 'en');
$this->nodes['article']['en'][2] = $this->createNode('article', 'en');
$this->nodes['article']['en'][1] = $this->createNode('article', 'en');
$this->nodes['article']['en'][0] = $this->createNode('article', 'en');
}
/**
* Tests that the term bundle filter works.
*/
function testTermBundleFilter() {
$vocabulary1 = entity_create('taxonomy_vocabulary', array(
'machine_name' => 'vocab1',
'name' => $this->randomName(),
));
taxonomy_vocabulary_save($vocabulary1);
$term1 = entity_create('taxonomy_term', array(
'name' => $this->randomName(),
'vid' => $vocabulary1->vid,
));
taxonomy_term_save($term1);
$vocabulary2 = (object) array(
'machine_name' => 'vocab2',
'name' => $this->randomName(),
);
taxonomy_vocabulary_save($vocabulary2);
$term2 = entity_create('taxonomy_term', array(
'name' => $this->randomName(),
'vid' => $vocabulary2->vid,
));
taxonomy_term_save($term2);
$this->drupalGet('admin/tmgmt/sources/entity_taxonomy_term');
// Both terms should be displayed with their bundle.
$this->assertText($term1->name);
$this->assertText($term2->name);
$this->assertTrue($this->xpath('//td[text()=@vocabulary]', array('@vocabulary' => $vocabulary1->name)));
$this->assertTrue($this->xpath('//td[text()=@vocabulary]', array('@vocabulary' => $vocabulary2->name)));
// Limit to the first vocabulary.
$edit = array();
$edit['search[vocabulary_machine_name]'] = $vocabulary1->machine_name;
$this->drupalPost(NULL, $edit, t('Search'));
// Only term 1 should be displayed now.
$this->assertText($term1->name);
$this->assertNoText($term2->name);
$this->assertTrue($this->xpath('//td[text()=@vocabulary]', array('@vocabulary' => $vocabulary1->name)));
$this->assertFalse($this->xpath('//td[text()=@vocabulary]', array('@vocabulary' => $vocabulary2->name)));
}
function testAvailabilityOfEntityLists() {
$this->drupalGet('admin/tmgmt/sources/entity_comment');
// Check if we are at comments page.
$this->assertText(t('Comment overview (Entity)'));
// No comments yet - empty message is expected.
$this->assertText(t('No entities matching given criteria have been found.'));
$this->drupalGet('admin/tmgmt/sources/entity_node');
// Check if we are at nodes page.
$this->assertText(t('Node overview (Entity)'));
// We expect article title as article node type is entity translatable.
$this->assertText($this->nodes['article']['en'][0]->title);
// Page node type should not be listed as it is not entity translatable.
$this->assertNoText($this->nodes['page']['en'][0]->title);
}
function testTranslationStatuses() {
// Test statuses: Source, Missing.
$this->drupalGet('admin/tmgmt/sources/entity_node');
$langstatus_en = $this->xpath('//table[@id="tmgmt-entities-list"]/tbody/tr[1]/td[@class="langstatus-en"]');
$langstatus_de = $this->xpath('//table[@id="tmgmt-entities-list"]/tbody/tr[1]/td[@class="langstatus-de"]');
$this->assertEqual($langstatus_en[0]->div['title'], t('Source language'));
$this->assertEqual($langstatus_de[0]->div['title'], t('Not translated'));
// Test status: Active job item.
$job = $this->createJob('en', 'de');
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
$job->addItem('entity', 'node', $this->nodes['article']['en'][0]->nid);
$job->requestTranslation();
$this->drupalGet('admin/tmgmt/sources/entity_node');
$langstatus_de = $this->xpath('//table[@id="tmgmt-entities-list"]/tbody/tr[1]/td[@class="langstatus-de"]/a');
$items = $job->getItems();
$wrapper = entity_metadata_wrapper('tmgmt_job_item', array_shift($items));
$label = t('Active job item: @state', array('@state' => $wrapper->state->label()));
$this->assertEqual($langstatus_de[0]->div['title'], $label);
// Test status: Current
foreach ($job->getItems() as $job_item) {
$job_item->acceptTranslation();
}
$this->drupalGet('admin/tmgmt/sources/entity_node');
$langstatus_de = $this->xpath('//table[@id="tmgmt-entities-list"]/tbody/tr[1]/td[@class="langstatus-de"]');
$this->assertEqual($langstatus_de[0]->div['title'], t('Translation up to date'));
}
function testTranslationSubmissions() {
// Simple submission.
$nid = $this->nodes['article']['en'][0]->nid;
$edit = array();
$edit["items[$nid]"] = 1;
$this->drupalPost('admin/tmgmt/sources/entity_node', $edit, t('Request translation'));
$this->assertText(t('One job needs to be checked out.'));
// Submission of two entities of the same source language.
$nid1 = $this->nodes['article']['en'][0]->nid;
$nid2 = $this->nodes['article']['en'][1]->nid;
$edit = array();
$edit["items[$nid1]"] = 1;
$edit["items[$nid2]"] = 1;
$this->drupalPost('admin/tmgmt/sources/entity_node', $edit, t('Request translation'));
$this->assertText(t('One job needs to be checked out.'));
// Submission of several entities of different source languages.
$nid1 = $this->nodes['article']['en'][0]->nid;
$nid2 = $this->nodes['article']['en'][1]->nid;
$nid3 = $this->nodes['article']['en'][2]->nid;
$nid4 = $this->nodes['article']['en'][3]->nid;
$nid5 = $this->nodes['article']['de'][0]->nid;
$nid6 = $this->nodes['article']['fr'][0]->nid;
$edit = array();
$edit["items[$nid1]"] = 1;
$edit["items[$nid2]"] = 1;
$edit["items[$nid3]"] = 1;
$edit["items[$nid4]"] = 1;
$edit["items[$nid5]"] = 1;
$edit["items[$nid6]"] = 1;
$this->drupalPost('admin/tmgmt/sources/entity_node', $edit, t('Request translation'));
$this->assertText(t('@count jobs need to be checked out.', array('@count' => '3')));
}
function testNodeEntityListings() {
// Turn off the entity translation.
$edit = array();
$edit['language_content_type'] = TRANSLATION_ENABLED;
$this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type'));
// Check if we have appropriate message in case there are no entity
// translatable content types.
$this->drupalGet('admin/tmgmt/sources/entity_node');
$this->assertText(t('Entity translation is not enabled for any of existing content types. To use this functionality go to Content types administration and enable entity translation for desired content types.'));
// Turn on the entity translation for both - article and page - to test
// search form.
$edit = array();
$edit['language_content_type'] = ENTITY_TRANSLATION_ENABLED;
$this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type'));
$this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type'));
// Create page node after entity translation is enabled.
$page_node_translatable = $this->createNode('page');
$this->drupalGet('admin/tmgmt/sources/entity_node');
// We have both listed - one of articles and page.
$this->assertText($this->nodes['article']['en'][0]->title);
$this->assertText($page_node_translatable->title);
// Try the search by content type.
$edit = array();
$edit['search[type]'] = 'article';
$this->drupalPost('admin/tmgmt/sources/entity_node', $edit, t('Search'));
// There should be article present.
$this->assertText($this->nodes['article']['en'][0]->title);
// The page node should not be listed.
$this->assertNoText($page_node_translatable->title);
// Try cancel button - despite we do post content type search value
// we should get nodes of botch content types.
$this->drupalPost('admin/tmgmt/sources/entity_node', $edit, t('Cancel'));
$this->assertText($this->nodes['article']['en'][0]->title);
$this->assertText($page_node_translatable->title);
}
function testEntitySourceListSearch() {
// We need a node with title composed of several words to test
// "any words" search.
$title_part_1 = $this->randomName('4');
$title_part_2 = $this->randomName('4');
$title_part_3 = $this->randomName('4');
$this->nodes['article']['en'][0]->title = "$title_part_1 $title_part_2 $title_part_3";
node_save($this->nodes['article']['en'][0]);
// Submit partial node title and see if we have a result.
$edit = array();
$edit['search[title]'] = "$title_part_1 $title_part_3";
$this->drupalPost('admin/tmgmt/sources/entity_node', $edit, t('Search'));
$this->assertText("$title_part_1 $title_part_2 $title_part_3", 'Searching on partial node title must return the result.');
// Check if there is only one result in the list.
$search_result_rows = $this->xpath('//table[@id="tmgmt-entities-list"]/tbody/tr');
$this->assert(count($search_result_rows) == 1, 'The search result must return only one row.');
// To test if other entity types work go for simple comment search.
$comment = new stdClass();
$comment->comment_body[LANGUAGE_NONE][0]['value'] = $this->randomName();
$comment->subject = $this->randomName();
// We need to associate the comment with entity translatable node object.
$comment->nid = $this->nodes['article']['en'][0]->nid;
// Set defaults - without these we will get Undefined property notices.
$comment->is_anonymous = TRUE;
$comment->cid = 0;
$comment->pid = 0;
$comment->uid = 0;
// Will add further comment variables.
$comment = comment_submit($comment);
comment_save($comment);
// Do search for the comment.
$edit = array();
$edit['search[subject]'] = $comment->subject;
$this->drupalPost('admin/tmgmt/sources/entity_comment', $edit, t('Search'));
$this->assertText($comment->subject, 'Searching for a comment subject.');
}
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* @file
* Main module file for the translation management entity source plugin user
* interface.
*/
/**
* Implements hook_page_alter().
*/
function tmgmt_entity_ui_page_alter(&$page) {
if (entity_access('create', 'tmgmt_job')) {
// Translation tabs for nodes.
if (isset($page['content']['system_main']['entity_translation_overview'])) {
module_load_include('inc', 'tmgmt_entity_ui', 'tmgmt_entity_ui.pages');
$page['content']['system_main']['entity_translation_overview'] = drupal_get_form('tmgmt_entity_ui_translate_form', $page['content']['system_main']);
}
// Support the context module: when context is used for placing the
// system_main block, then block contents appear nested one level deeper
// under another 'content' key.
elseif (isset($page['content']['system_main']['content']['entity_translation_overview'])) {
module_load_include('inc', 'tmgmt_entity_ui', 'tmgmt_entity_ui.pages');
$page['content']['system_main']['content']['entity_translation_overview'] = drupal_get_form('tmgmt_entity_ui_translate_form', $page['content']['system_main']['content']);
}
}
}
/**
* Implements tmgmt_entity_tmgmt_source_plugin_info_alter().
*/
function tmgmt_entity_ui_tmgmt_source_plugin_info_alter(&$info) {
// Define ui controller class to handle Drupal entities.
$info['entity']['ui controller class'] = 'TMGMTEntitySourceUIController';
// Alter file and file path info so that tmgmt_entity_ui module is targeted
// for page callback.
$info['entity']['file'] = 'tmgmt_entity_ui.pages.inc';
$info['entity']['file path'] = drupal_get_path('module', 'tmgmt_entity_ui');
}

View File

@@ -0,0 +1,135 @@
<?php
/**
* @file
* Provides page and form callbacks for the Translation Management Tool Entity
* Source User Interface module.
*/
/**
* Entity translation overview form.
*/
function tmgmt_entity_ui_translate_form($form, &$form_state, $build) {
// Store the entity in the form state so we can easily create the job in the
// submit handler.
$form_state['entity_type'] = $build['#entity_type'];
$form_state['entity'] = $build['#entity'];
$overview = $build['entity_translation_overview'];
list($id, $vid, $bundle) = entity_extract_ids($form_state['entity_type'], $form_state['entity']);
$form['top_actions']['#type'] = 'actions';
$form['top_actions']['#weight'] = -10;
tmgmt_ui_add_cart_form($form['top_actions'], $form_state, 'entity', $build['#entity_type'], $id);
// Inject our additional column into the header.
array_splice($overview['#header'], -1, 0, array(t('Pending Translations')));
// Make this a tableselect form.
$form['languages'] = array(
'#type' => 'tableselect',
'#header' => $overview['#header'],
'#options' => array(),
);
$languages = language_list();
// Check if there is a job / job item that references this translation.
$entity_language = entity_language($form_state['entity_type'], $form_state['entity']);
$items = tmgmt_job_item_load_latest('entity', $form_state['entity_type'], $id, $entity_language);
foreach ($languages as $langcode => $language) {
if ($langcode == LANGUAGE_NONE) {
// Never show language neutral on the overview.
continue;
}
// Since the keys are numeric and in the same order we can shift one element
// after the other from the original non-form rows.
$option = array_shift($overview['#rows']);
if ($langcode == $entity_language) {
$additional = '<strong>' . t('Source') . '</strong>';
// This is the source object so we disable the checkbox for this row.
$form['languages'][$langcode] = array(
'#type' => 'checkbox',
'#disabled' => TRUE,
);
}
elseif (isset($items[$langcode])) {
$item = $items[$langcode];
$uri = $item->uri();
$wrapper = entity_metadata_wrapper('tmgmt_job_item', $item);
$additional = l($wrapper->state->label(), $uri['path'], array('query' => array('destination' => current_path())));
// Disable the checkbox for this row since there is already a translation
// in progress that has not yet been finished. This way we make sure that
// we don't stack multiple active translations for the same item on top
// of each other.
$form['languages'][$langcode] = array(
'#type' => 'checkbox',
'#disabled' => TRUE,
);
}
else {
// There is no translation job / job item for this target language.
$additional = t('None');
}
// Inject the additional column into the array.
// The generated form structure has changed, support both an additional
// 'data' key (that is not supported by tableselect) and the old version
// without.
if (isset($option['data'])) {
array_splice($option['data'], -1, 0, array($additional));
// Append the current option array to the form.
$form['languages']['#options'][$langcode] = $option['data'];
}
else {
array_splice($option, -1, 0, array($additional));
// Append the current option array to the form.
$form['languages']['#options'][$langcode] = $option;
}
}
$form['actions']['#type'] = 'actions';
$form['actions']['request'] = array(
'#type' => 'submit',
'#value' => t('Request translation'),
'#submit' => array('tmgmt_entity_ui_translate_form_submit'),
'#validate' => array('tmgmt_entity_ui_translate_form_validate'),
);
return $form;
}
/**
* Validation callback for the entity translation overview form.
*/
function tmgmt_entity_ui_translate_form_validate($form, &$form_state) {
$selected = array_filter($form_state['values']['languages']);
if (empty($selected)) {
form_set_error('languages', t('You have to select at least one language for requesting a translation.'));
}
}
/**
* Submit callback for the entity translation overview form.
*/
function tmgmt_entity_ui_translate_form_submit($form, &$form_state) {
$entity = $form_state['entity'];
$entity_type = $form_state['entity_type'];
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
$values = $form_state['values'];
$jobs = array();
foreach (array_keys(array_filter($values['languages'])) as $langcode) {
// Create the job object.
$job = tmgmt_job_create(entity_language($entity_type, $entity), $langcode, $GLOBALS['user']->uid);
try {
// Add the job item.
$job->addItem('entity', $entity_type, $id);
// Append this job to the array of created jobs so we can redirect the user
// to a multistep checkout form if necessary.
$jobs[$job->tjid] = $job;
}
catch (TMGMTException $e) {
watchdog_exception('tmgmt', $e);
$languages = language_list();
$target_lang_name = $languages[$langcode]->language;
drupal_set_message(t('Unable to add job item for target language %name. Make sure the source content is not empty.', array('%name' => $target_lang_name)), 'error');
}
}
tmgmt_ui_job_checkout_and_redirect($form_state, $jobs);
}

View File

@@ -0,0 +1,328 @@
<?php
/**
* Basic Node Source tests.
*
*/
class TMGMTEntitySourceUITestCase extends TMGMTEntityTestCaseUtility {
static function getInfo() {
return array(
'name' => 'Entity Source UI tests',
'description' => 'Tests the user interface for entity translation sources.',
'group' => 'Translation Management',
'dependencies' => array('entity_translation'),
);
}
function setUp() {
parent::setUp(array('tmgmt_entity_ui', 'block', 'comment'));
variable_set('language_content_type_page', ENTITY_TRANSLATION_ENABLED);
variable_set('language_content_type_article', ENTITY_TRANSLATION_ENABLED);
$this->loginAsAdmin(array(
'create translation jobs',
'submit translation jobs',
'accept translation jobs',
'administer blocks',
'administer entity translation',
'toggle field translatability',
));
$this->setEnvironment('de');
$this->setEnvironment('fr');
$this->setEnvironment('es');
$this->setEnvironment('el');
$this->createNodeType('page', st('Page'), ENTITY_TRANSLATION_ENABLED);
$this->createNodeType('article', st('Article'), ENTITY_TRANSLATION_ENABLED);
// Enable path locale detection.
$edit = array(
'language[enabled][locale-url]' => TRUE,
'language_content[enabled][locale-interface]' => TRUE,
);
$this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings'));
// @todo Re-enable this when switching to testing profile.
// Enable the main page content block for hook_page_alter() to work.
$edit = array(
'blocks[system_main][region]' => 'content',
);
$this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
}
/**
* Test the translate tab for a single checkout.
*/
function testNodeTranslateTabSingleCheckout() {
$this->loginAsTranslator(array('translate node entities'));
// Create an english source node.
$node = $this->createNode('page', 'en');
// Create a nodes that will not be translated to test the missing
// translation filter.
$node_not_translated = $this->createNode('page', 'en');
$node_german = $this->createNode('page', 'de');
// Go to the translate tab.
$this->drupalGet('node/' . $node->nid);
$this->clickLink('Translate');
// Assert some basic strings on that page.
$this->assertText(t('Translations of @title', array('@title' => $node->title)));
$this->assertText(t('Pending Translations'));
// Request a translation for german.
$edit = array(
'languages[de]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
// Verify that we are on the translate tab.
$this->assertText(t('One job needs to be checked out.'));
$this->assertText($node->title);
// Submit.
$this->drupalPost(NULL, array(), t('Submit to translator'));
// Make sure that we're back on the translate tab.
$this->assertEqual(url('node/' . $node->nid . '/translate', array('absolute' => TRUE)), $this->getUrl());
$this->assertText(t('Test translation created.'));
$this->assertText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node->title, '@language' => t('German'))));
// Verify that the pending translation is shown.
$this->clickLink(t('Needs review'));
$this->drupalPost(NULL, array(), t('Save as completed'));
$this->assertText(t('The translation for @title has been accepted.', array('@title' => $node->title)));
// German node should now be listed and be clickable.
// @todo Improve detection of the link, e.g. use xpath on the table or the
// title module to get a better title.
$this->clickLink('view', 1);
$this->assertText('de_' . $node->body['en'][0]['value']);
// Test that the destination query argument does not break the redirect
// and we are redirected back to the correct page.
$this->drupalGet('node/' . $node->nid . '/translate', array('query' => array('destination' => 'node')));
// Request a spanish translation.
$edit = array(
'languages[es]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
// Verify that we are on the checkout page.
$this->assertText(t('One job needs to be checked out.'));
$this->assertText($node->title);
$this->drupalPost(NULL, array(), t('Submit to translator'));
// Make sure that we're back on the originally defined destination URL.
$this->assertEqual(url('node', array('absolute' => TRUE)), $this->getUrl());
// Test the missing translation filter.
$this->drupalGet('admin/tmgmt/sources');
$this->assertText($node->title);
$this->assertText($node_not_translated->title);
$this->drupalPost(NULL, array(
'search[target_language]' => 'de',
'search[target_status]' => 'untranslated',
), t('Search'));
$this->assertNoText($node->title);
$this->assertNoText($node_german->title);
$this->assertText($node_not_translated->title);
// Update the the translate flag of the translated node and test if it is
// listed among sources with missing translation.
db_update('entity_translation')->fields(array('translate' => 1))
->condition('entity_type', 'node')->condition('entity_id', $node->nid)->execute();
$this->drupalPost(NULL, array(
'search[target_language]' => 'de',
'search[target_status]' => 'outdated',
), t('Search'));
$this->assertText($node->title);
$this->assertNoText($node_german->title);
$this->assertNoText($node_not_translated->title);
$this->drupalPost(NULL, array(
'search[target_language]' => 'de',
'search[target_status]' => 'untranslated_or_outdated',
), t('Search'));
$this->assertText($node->title);
$this->assertNoText($node_german->title);
$this->assertText($node_not_translated->title);
}
/**
* Test the translate tab for a single checkout.
*/
function testNodeTranslateTabMultipeCheckout() {
// Allow auto-accept.
$default_translator = tmgmt_translator_load('test_translator');
$default_translator->settings = array(
'auto_accept' => TRUE,
);
$default_translator->save();
$this->loginAsTranslator(array('translate node entities'));
// Create an english source node.
$node = $this->createNode('page', 'en');
// Go to the translate tab.
$this->drupalGet('node/' . $node->nid);
$this->clickLink('Translate');
// Assert some basic strings on that page.
$this->assertText(t('Translations of @title', array('@title' => $node->title)));
$this->assertText(t('Pending Translations'));
// Request a translation for german.
$edit = array(
'languages[de]' => TRUE,
'languages[es]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
// Verify that we are on the translate tab.
$this->assertText(t('2 jobs need to be checked out.'));
// Submit all jobs.
$this->assertText($node->title);
$this->drupalPost(NULL, array(), t('Submit to translator and continue'));
$this->assertText($node->title);
$this->drupalPost(NULL, array(), t('Submit to translator'));
// Make sure that we're back on the translate tab.
$this->assertEqual(url('node/' . $node->nid . '/translate', array('absolute' => TRUE)), $this->getUrl());
$this->assertText(t('Test translation created.'));
$this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node->title, '@language' => t('Spanish'))));
$this->assertText(t('The translation for @title has been accepted.', array('@title' => $node->title)));
// Translated nodes should now be listed and be clickable.
// @todo Use links on translate tab.
$this->drupalGet('de/node/' . $node->nid);
$this->assertText('de_' . $node->body['en'][0]['value']);
$this->drupalGet('es/node/' . $node->nid);
$this->assertText('es_' . $node->body['en'][0]['value']);
}
/**
* Test translating comments.
*
* @todo: Disabled pending resolution of http://drupal.org/node/1760270.
*/
function dtestCommentTranslateTab() {
// Login as admin to be able to submit config page.
$this->loginAsAdmin(array('administer entity translation'));
// Enable comment translation.
$edit = array(
'entity_translation_entity_types[comment]' => TRUE
);
$this->drupalPost('admin/config/regional/entity_translation', $edit, t('Save configuration'));
// Change comment_body field to be translatable.
$comment_body = field_info_field('comment_body');
$comment_body['translatable'] = TRUE;
field_update_field($comment_body);
// Create a user that is allowed to translate comments.
$permissions = array('translate comment entities', 'create translation jobs', 'submit translation jobs', 'accept translation jobs', 'post comments', 'skip comment approval', 'edit own comments', 'access comments');
$entity_translation_permissions = entity_translation_permission();
// The new translation edit form of entity_translation requires a new
// permission that does not yet exist in older versions. Add it
// conditionally.
if (isset($entity_translation_permissions['edit original values'])) {
$permissions[] = 'edit original values';
}
$this->loginAsTranslator($permissions, TRUE);
// Create an english source term.
$node = $this->createNode('article', 'en');
// Add a comment.
$this->drupalGet('node/' . $node->nid);
$edit = array(
'subject' => $this->randomName(),
'comment_body[en][0][value]' => $this->randomName(),
);
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertText(t('Your comment has been posted.'));
// Go to the translate tab.
$this->clickLink('edit');
$this->assertTrue(preg_match('|comment/(\d+)/edit$|', $this->getUrl(), $matches), 'Comment found');
$comment = comment_load($matches[1]);
$this->clickLink('Translate');
// Assert some basic strings on that page.
$this->assertText(t('Translations of @title', array('@title' => $comment->subject)));
$this->assertText(t('Pending Translations'));
// Request a translation for german.
$edit = array(
'languages[de]' => TRUE,
'languages[es]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
// Verify that we are on the translate tab.
$this->assertText(t('2 jobs need to be checked out.'));
// Submit all jobs.
$this->assertText($comment->subject);
$this->drupalPost(NULL, array(), t('Submit to translator and continue'));
$this->assertText($comment->subject);
$this->drupalPost(NULL, array(), t('Submit to translator'));
// Make sure that we're back on the translate tab.
$this->assertEqual(url('comment/' . $comment->cid . '/translate', array('absolute' => TRUE)), $this->getUrl());
$this->assertText(t('Test translation created.'));
$this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $comment->subject, '@language' => t('Spanish'))));
$this->assertText(t('The translation for @title has been accepted.', array('@title' => $comment->subject)));
// @todo Use links on translate tab.
$this->drupalGet('de/comment/' . $comment->cid);
$this->assertText('de_' . $comment->comment_body['en'][0]['value']);
// @todo Use links on translate tab.
$this->drupalGet('es/node/' . $comment->cid);
$this->assertText('es_' . $comment->comment_body['en'][0]['value']);
}
/**
* Test the entity source specific cart functionality.
*/
function testCart() {
$this->loginAsTranslator(array('translate node entities'));
$nodes = array();
for ($i = 0; $i < 4; $i++) {
$nodes[$i] = $this->createNode('page');
}
// Test the source overview.
$this->drupalGet('admin/tmgmt/sources/entity');
$this->drupalPost('admin/tmgmt/sources/entity', array(
'items[' . $nodes[1]->nid . ']' => TRUE,
'items[' . $nodes[2]->nid . ']' => TRUE,
), t('Add to cart'));
$this->drupalGet('admin/tmgmt/cart');
$this->assertText($nodes[1]->title);
$this->assertText($nodes[2]->title);
// Test the translate tab.
$this->drupalGet('node/' . $nodes[3]->nid . '/translate');
$this->assertRaw(t('There are @count items in the <a href="@url">translation cart</a>.',
array('@count' => 2, '@url' => url('admin/tmgmt/cart'))));
$this->drupalPost(NULL, array(), t('Add to cart'));
$this->assertRaw(t('@count content source was added into the <a href="@url">cart</a>.', array('@count' => 1, '@url' => url('admin/tmgmt/cart'))));
$this->assertRaw(t('There are @count items in the <a href="@url">translation cart</a> including the current item.',
array('@count' => 3, '@url' => url('admin/tmgmt/cart'))));
}
}

View File

@@ -0,0 +1,174 @@
<?php
/**
* Generic entity ui controller class for source plugin.
*
* @ingroup tmgmt_source
*/
class TMGMTEntitySourceUIController extends TMGMTEntityDefaultSourceUIController {
/**
* Gets overview form header.
*
* @return array
* Header array definition as expected by theme_tablesort().
*/
public function overviewFormHeader($type) {
$languages = array();
foreach (language_list() as $langcode => $language) {
$languages['langcode-' . $langcode] = array(
'data' => check_plain($language->name),
);
}
$entity_info = entity_get_info($type);
$header = array(
'title' => array('data' => t('Title (in source language)')),
);
// Show the bundle if there is more than one for this entity type.
if (count(tmgmt_entity_get_translatable_bundles($type)) > 1) {
$header['bundle'] = array('data' => t('@entity_name type', array('@entity_name' => $entity_info['label'])));
}
$header += $languages;
return $header;
}
/**
* Builds a table row for overview form.
*
* @param array $data
* Data needed to build the list row.
*
* @return array
*/
public function overviewRow($data) {
$label = $data['entity_label'] ? $data['entity_label'] : t('@type: @id', array('@type' => $data['entity_type'], '@id' => $data['entity_id']));
$row = array(
'id' => $data['entity_id'],
'title' => l($label, $data['entity_uri']),
);
if (isset($data['bundle'])) {
$row['bundle'] = $data['bundle'];
}
foreach (language_list() as $langcode => $language) {
$row['langcode-' . $langcode] = array(
'data' => theme('tmgmt_ui_translation_language_status_single', array(
'translation_status' => $data['translation_statuses'][$langcode],
'job_item' => isset($data['current_job_items'][$langcode]) ? $data['current_job_items'][$langcode] : NULL,
)),
'class' => array('langstatus-' . $langcode),
);
}
return $row;
}
/**
* {@inheritdoc}
*/
public function overviewForm($form, &$form_state, $type) {
$form += $this->overviewSearchFormPart($form, $form_state, $type);
$form['items'] = array(
'#type' => 'tableselect',
'#header' => $this->overviewFormHeader($type),
'#empty' => t('No entities matching given criteria have been found.'),
'#attributes' => array('id' => 'tmgmt-entities-list'),
);
// Load search property params which will be passed into
$search_property_params = array();
$exclude_params = array('q', 'page');
foreach ($_GET as $key => $value) {
// Skip exclude params, and those that have empty values, as these would
// make it into query condition instead of being ignored.
if (in_array($key, $exclude_params) || $value === '') {
continue;
}
$search_property_params[$key] = $value;
}
foreach ($this->getEntitiesTranslationData($type, $search_property_params) as $data) {
$form['items']['#options'][$data['entity_id']] = $this->overviewRow($data);
}
$form['pager'] = array('#markup' => theme('pager', array('tags' => NULL)));
return $form;
}
/**
* {@inheritdoc}
*/
public function overviewFormValidate($form, &$form_state, $type) {
if (!empty($form_state['values']['search']['target_language']) && $form_state['values']['search']['language'] == $form_state['values']['search']['target_language']) {
form_set_error('search[target_language]', t('The source and target languages must not be the same.'));
}
}
/**
* {@inheritdoc}
*/
public function overviewFormSubmit($form, &$form_state, $type) {
// Handle search redirect.
$this->overviewSearchFormRedirect($form, $form_state, $type);
$jobs = array();
$entities = entity_load($type, $form_state['values']['items']);
$source_lang_registry = array();
// Loop through entities and create individual jobs for each source language.
foreach ($entities as $entity) {
/**
* @var EntityTranslationDefaultHandler $handler
*/
$handler = entity_translation_get_handler($type, $entity);
$source_lang = entity_language($type, $entity);
list($entity_id, ,) = entity_extract_ids($type, $entity);
try {
// For given source lang no job exists yet.
if (!isset($source_lang_registry[$source_lang])) {
// Create new job.
$job = tmgmt_job_create($source_lang, NULL, $GLOBALS['user']->uid);
// Add initial job item.
$job->addItem('entity', $type, $entity_id);
// Add job identifier into registry
$source_lang_registry[$source_lang] = $job->tjid;
// Add newly created job into jobs queue.
$jobs[$job->tjid] = $job;
}
// We have a job for given source lang, so just add new job item for the
// existing job.
else {
$jobs[$source_lang_registry[$source_lang]]->addItem('entity', $type, $entity_id);
}
}
catch (TMGMTException $e) {
watchdog_exception('tmgmt', $e);
$entity_label = entity_label($type, $entity);
drupal_set_message(t('Unable to add job item for entity %name: %error.', array('%name' => $entity_label, '%error' => $e->getMessage())), 'error');
}
}
// If necessary, do a redirect.
$redirects = tmgmt_ui_job_checkout_multiple($jobs);
if ($redirects) {
tmgmt_ui_redirect_queue_set($redirects, current_path());
$form_state['redirect'] = tmgmt_ui_redirect_queue_dequeue();
drupal_set_message(format_plural(count($redirects), t('One job needs to be checked out.'), t('@count jobs need to be checked out.')));
}
}
}

View File

@@ -0,0 +1,106 @@
<?php
/*
* @file
* API documentation for this module.
*/
/**
* Alter source field data before being saved in a translation job.
*
* @param array $data
* Source field data.
* @param string $entity_type
* The entity type.
* @param object $entity
* An entity object.
* @param string $langcode
* The language of retrieved field values.
*/
function hook_tmgmt_field_source_data_alter(&$data, $entity_type, $entity, $langcode) {
}
/**
* Extract translatable text elements from a field.
*
* @param $entity_type
* The type of $entity.
* @param $entity
* The entity being extracted.
* @param $field
* The field structure.
* @param $instance
* The field instance.
* @param $langcode
* The language associated with $items.
* @param $items
* Array of values for this field.
*
* @return
* An array of translatable text elements, keyed by the schema name of the
* field.
*
* @see text_tmgmt_source_translation_structure()
*
* @ingroup tmgmt_source
*/
function hook_tmgmt_source_translation_structure($entity_type, $entity, $field, $instance, $langcode, $items) {
}
/**
* Puts data on the entity of the field type owned by the module.
*
* @param $entity_type
* The type of $entity.
* @param $entity
* The entity being extracted.
* @param $field
* The field structure.
* @param $instance
* The field instance.
* @param $langcode
* The language associated with $items.
* @param $data
* Translated data array.
* @param $use_field_translation
* TRUE if field translation is being used.
*
* @see tmgmt_field_populate_entity()
*/
function hook_tmgmt_field_type_populate_entity($entity_type, $entity, $field, $instance, $langcode, $data, $use_field_translation) {
}
/**
* Alter an entity object before populating its field with translations.
*
* @param array $data
* Source field data.
* @param object $entity
* An entity object.
* @param string $entity_type
* The entity type.
* @param string $langcode
* The language of retrieved field values.
*/
function hook_tmgmt_field_pre_populate_entity_alter(&$data, $entity, $entity_type, $langcode) {
}
/**
* Alter an entity object after populating its field with translations.
*
* @param object $entity
* An entity object.
* @param string $entity_type
* The entity type.
* @param array $data
* Source field data.
* @param string $langcode
* The language of retrieved field values.
*/
function hook_tmgmt_field_post_populate_entity_alter(&$entity, $entity_type, $data, $langcode) {
}

View File

@@ -0,0 +1,12 @@
name = Translation Management Field
description = Implements some hooks on behalf of the core Field modules that are required in the Translation Management module.
package = Translation Management
core = 7.x
test_dependencies[] = field_collection
; Information added by Drupal.org packaging script on 2016-09-21
version = "7.x-1.0-rc2+1-dev"
core = "7.x"
project = "tmgmt"
datestamp = "1474446494"

View File

@@ -0,0 +1,140 @@
<?php
/**
* @file
* Main module file for the Translation Management Source Field module.
*/
/**
* Implements hook_tmgmt_source_translation_structure().
*
* This hook is implemented on behalf of the core text module.
*/
function text_tmgmt_source_translation_structure($entity_type, $entity, $field, $instance, $langcode, $items) {
$structure = array();
if (!empty($items)) {
$structure['#label'] = check_plain($instance['label']);
foreach ($items as $delta => $value) {
$structure[$delta]['#label'] = t('Delta #@delta', array('@delta' => $delta));
$structure[$delta]['value'] = array(
'#label' => $structure['#label'],
'#text' => $value['value'],
'#translate' => TRUE,
);
// Add format.
$structure[$delta]['format'] = array(
'#label' => '',
'#text' => $value['format'],
'#translate' => FALSE,
);
if ($field['type'] == 'text_with_summary' && !empty($value['summary'])) {
$structure[$delta]['summary'] = array(
'#label' => t('Summary'),
'#text' => $value['summary'],
'#translate' => TRUE,
);
}
}
}
return $structure;
}
/**
* Helper function for retrieving all translatable field values from an entity.
*
* @param $entity_type
* The entity type.
* @param $entity
* An entity object.
* @param $langcode
* The language of retrieved field values.
* @param $only_translatable
* If TRUE, only the fields which are flagged as translatable are returned.
* Defaults to FALSE, which is usually used for node translation, where the
* field translatability does not matter.
*
* @return array
* The structured field data for all translatable fields
*/
function tmgmt_field_get_source_data($entity_type, $entity, $langcode, $only_translatable = FALSE) {
try {
list(, , $bundle) = entity_extract_ids($entity_type, $entity);
}
catch (Exception $e) {
watchdog_exception('tmgmt field', $e);
return array();
}
$fields = array();
foreach (field_info_instances($entity_type, $bundle) as $field_name => $instance) {
$field = field_info_field($field_name);
$items = field_get_items($entity_type, $entity, $field_name, $langcode);
if ((!$only_translatable || $field['translatable']) && $items) {
if ($data = module_invoke($field['module'], 'tmgmt_source_translation_structure', $entity_type, $entity, $field, $instance, $langcode, $items)) {
$fields[$field_name] = $data;
}
}
}
drupal_alter('tmgmt_field_source_data', $fields, $entity_type, $entity, $langcode);
return $fields;
}
/**
* Populates a field on an object with the provided field values.
*
* @param $entity_type
* The type of $entity.
* @param $entity
* The object to be populated.
* @param $langcode
* The field language.
* @param $data
* An array of values.
* @param $use_field_translation
* TRUE if field translation is being used.
*/
function tmgmt_field_populate_entity($entity_type, $entity, $langcode, $data, $use_field_translation = TRUE) {
drupal_alter('tmgmt_field_pre_populate_entity', $data, $entity, $entity_type, $langcode);
foreach (element_children($data) as $field_name) {
if ($field = field_info_field($field_name)) {
$function = $field['module'] . '_field_type_tmgmt_populate_entity';
list(, , $bundle) = entity_extract_ids($entity_type, $entity);
$instance = field_info_instance($entity_type, $field_name, $bundle);
if (function_exists($function)) {
$function($entity_type, $entity, $field, $instance, $langcode, $data, $use_field_translation);
}
else {
$field_langcode = $field['translatable'] ? $langcode : LANGUAGE_NONE;
// When not using field translation, make sure we're not storing
// multiple languages.
if (!$use_field_translation) {
$entity->{$field_name} = array($field_langcode => array());
}
foreach (element_children($data[$field_name]) as $delta) {
$columns = array();
foreach (element_children($data[$field_name][$delta]) as $column) {
if (isset($data[$field_name][$delta][$column]['#translation']['#text'])) {
$columns[$column] = $data[$field_name][$delta][$column]['#translation']['#text'];
}
// For elements which are not translatable, keep using the original
// value.
elseif (isset($data[$field_name][$delta][$column]['#translate']) && $data[$field_name][$delta][$column]['#translate'] == FALSE) {
$columns[$column] = $data[$field_name][$delta][$column]['#text'];
}
}
// Make sure the array_merge() gets an array as a first parameter.
if (!isset($entity->{$field_name}[$field_langcode][$delta])) {
$entity->{$field_name}[$field_langcode][$delta] = array();
}
$entity->{$field_name}[$field_langcode][$delta] = array_merge($entity->{$field_name}[$field_langcode][$delta], $columns);
}
}
}
}
drupal_alter('tmgmt_field_post_populate_entity', $entity, $entity_type, $data, $langcode);
}

View File

@@ -0,0 +1,20 @@
name = i18n String Source
description = i18n String source plugin for the Translation Management system.
package = Translation Management
core = 7.x
dependencies[] = tmgmt_ui
dependencies[] = i18n_string
# List variable as a dependency so that it gets picked up testbot.
# See http://drupal.org/node/1440484
dependencies[] = i18n
dependencies[] = variable
files[] = tmgmt_i18n_string.plugin.inc
files[] = tmgmt_i18n_string.test
files[] = tmgmt_i18n_string.ui.inc
; Information added by Drupal.org packaging script on 2016-09-21
version = "7.x-1.0-rc2+1-dev"
core = "7.x"
project = "tmgmt"
datestamp = "1474446494"

View File

@@ -0,0 +1,342 @@
<?php
/**
* @file
* Source plugin for the Translation Management system that handles i18n strings.
*/
/**
* Implements hook_tmgmt_source_plugin_info().
*/
function tmgmt_i18n_string_tmgmt_source_plugin_info() {
$info['i18n_string'] = array(
'label' => t('i18n String'),
'description' => t('Source handler for i18n strings.'),
'plugin controller class' => 'TMGMTI18nStringSourcePluginController',
'ui controller class' => 'TMGMTI18nStringDefaultSourceUIController',
);
foreach (i18n_object_info() as $object_type => $object_info) {
// Only consider object types that have string translation information.
if (isset($object_info['string translation'])) {
$info['i18n_string']['item types'][$object_type] = $object_info['title'];
}
}
return $info;
}
/**
* Gets i18n strings for given type and label.
*
* @param string $type
* i18n object type.
* @param string $search_label
* Label to search for.
* @param string $target_language
* Target language.
* @param string $target_status
* Target status.
*
* @return array
* List of i18n strings data.
*/
function tmgmt_i18n_string_get_strings($type, $search_label = NULL, $target_language = NULL, $target_status = 'untranslated_or_outdated') {
$info = i18n_object_info($type);
$languages = drupal_map_assoc(array_keys(language_list()));
$select = db_select('i18n_string', 'i18n_s');
$select->addTag('tmgmt_sources_search');
$select->addMetaData('plugin', 'i18n_string');
$select->addMetaData('type', $type);
$select->condition('i18n_s.textgroup', $info['string translation']['textgroup']);
if (!empty($target_language) && in_array($target_language, $languages)) {
if ($target_status == 'untranslated_or_outdated') {
$or = db_or();
$or->isNull("lt_$target_language.language");
$or->condition("lt_$target_language.i18n_status", I18N_STRING_STATUS_UPDATE);
$select->condition($or);
}
elseif ($target_status == 'outdated') {
$select->condition("lt_$target_language.i18n_status", I18N_STRING_STATUS_UPDATE);
}
elseif ($target_status == 'untranslated') {
$select->isNull("lt_$target_language.language");
}
}
if (isset($info['string translation']['type'])) {
$select->condition('i18n_s.type', $info['string translation']['type']);
}
elseif ($type == 'field' || $type == 'field_instance') {
// Fields and field instances share the same textgroup. Use list of bundles
// to include/exclude field_instances.
$bundles = array();
foreach (entity_get_info() as $entity_info) {
$bundles = array_merge($bundles, array_keys($entity_info['bundles']));
}
$select->condition('i18n_s.objectid', $bundles, $type == 'field_instance' ? 'IN' : 'NOT IN');
}
$select->join('locales_source', 'ls', 'ls.lid = i18n_s.lid');
$select->addField('ls', 'source');
if (!empty($search_label)) {
$select->condition('ls.source', "%$search_label%", 'LIKE');
}
foreach ($languages as $langcode) {
$langcode = str_replace('-', '', $langcode);
$select->leftJoin('locales_target', "lt_$langcode", "i18n_s.lid = %alias.lid AND %alias.language = '$langcode'");
$select->addField("lt_$langcode", 'language', "lang_$langcode");
}
$select->fields("i18n_s", array('lid', 'textgroup', 'context', 'type', 'objectid'));
$select->addExpression("concat(i18n_s.textgroup, ':', i18n_s.type, ':', i18n_s.objectid)", 'job_item_id');
$select->orderBy('i18n_s.context');
$select->groupBy('type');
$select->groupBy('objectid');
$select = $select->extend('PagerDefault')->limit(variable_get('tmgmt_source_list_limit', 20));
return $select->execute()->fetchAll();
}
/**
* Implements hook_form_ID_alter().
*
* Adds request translation capabilities into i18n translate tab.
*/
function tmgmt_i18n_string_form_i18n_string_translate_page_overview_form_alter(&$form, &$form_state) {
$object = $form['object']['#value'];
// Create the id: textgroup:type:objectid.
$id = $object->get_textgroup() . ':' . implode(':', $object->get_string_context());
$source_language = variable_get_value('i18n_string_source_language');
$existing_items = tmgmt_job_item_load_latest('i18n_string', $object->get_type(), $id, $source_language);
$form['top_actions']['#type'] = 'actions';
$form['top_actions']['#weight'] = -10;
tmgmt_ui_add_cart_form($form['top_actions'], $form_state, 'i18n_string', $object->get_type(), $id);
$form['languages']['#type'] = 'tableselect';
// Append lang code so that we can use it
foreach ($form['languages']['#rows'] as $lang => $row) {
if (isset($existing_items[$lang])) {
$states = tmgmt_job_item_states();
$row['status'] = $states[$existing_items[$lang]->state];
if ($existing_items[$lang]->isNeedsReview()) {
$row['operations'] .= ' | ' . l(t('review'), 'admin/tmgmt/items/' . $existing_items[$lang]->tjiid, array('query' => array('destination' => $_GET['q'])));
}
elseif ($existing_items[$lang]->isActive()) {
$row['operations'] .= ' | ' . l(t('in progress'), 'admin/tmgmt/items/' . $existing_items[$lang]->tjiid, array('query' => array('destination' => $_GET['q'])));
}
}
$form['languages']['#options'][$id . ':' . $lang] = $row;
if ($lang == $source_language || isset($existing_items[$lang])) {
$form['languages'][$id . ':' . $lang] = array(
'#type' => 'checkbox',
'#disabled' => TRUE,
);
}
}
unset($form['languages']['#rows'], $form['languages']['#theme']);
$form['actions']['request_translation'] = array(
'#type' => 'submit',
'#value' => t('Request translation'),
'#submit' => array('tmgmt_i18n_string_translate_form_submit'),
'#validate' => array('tmgmt_i18n_string_translate_form_validate'),
);
}
/**
* Validation callback for the entity translation overview form.
*/
function tmgmt_i18n_string_translate_form_validate($form, &$form_state) {
$selected = array_filter($form_state['values']['languages']);
if (empty($selected)) {
form_set_error('languages', t('You have to select at least one language for requesting a translation.'));
}
}
function tmgmt_i18n_string_translate_form_submit($form, &$form_state) {
$items = array_filter($form_state['values']['languages']);
$type = $form_state['values']['object']->get_type();
$source_lang = variable_get_value('i18n_string_source_language');
$jobs = array();
$target_lang_registry = array();
// Loop through entities and create individual jobs for each source language.
foreach ($items as $item) {
$item_parts = explode(':', $item);
$target_lang = array_pop($item_parts);
$key = implode(':', $item_parts);
// For given source lang no job exists yet.
if (!isset($target_lang_registry[$target_lang])) {
// Create new job.
$job = tmgmt_job_create($source_lang, $target_lang, $GLOBALS['user']->uid);
// Add initial job item.
$job->addItem('i18n_string', $type, $key);
// Add job identifier into registry
$target_lang_registry[$target_lang] = $job->tjid;
// Add newly created job into jobs queue.
$jobs[$job->tjid] = $job;
}
// We have a job for given source lang, so just add new job item for the
// existing job.
else {
$jobs[$target_lang_registry[$target_lang]]->addItem('i18n_string', $type, $key);
}
}
tmgmt_ui_job_checkout_and_redirect($form_state, $jobs);
}
/**
* Implements hook_i18n_object_info_alter().
*/
function tmgmt_i18n_string_i18n_object_info_alter(&$info) {
$entity_info = entity_get_info();
// Add a entity key to the object info if neither load callback nor entity
// keys are set and the object is an entity_type.
// @todo: Add this as default in EntityDefaultI18nStringController.
foreach ($info as $name => &$object) {
if (!isset($object['load callback']) && !isset($object['entity']) && isset($entity_info[$name])) {
$object['entity'] = $name;
}
}
}
/**
* Returns the i18n wrapper object.
*
* I18N objects with one or two keys are supported.
*
* @param string $type
* I18n object type.
* @param object $i18n_string
* Object with type and objectid properties.
*
* @return i18n_string_object_wrapper
*/
function tmgmt_i18n_string_get_wrapper($type, $i18n_string) {
$object_key = i18n_object_info($type, 'key');
// Special handling for i18nviews.
if ($type == 'views') {
// The construct method needs the full view object.
$view = views_get_view($i18n_string->objectid);
$wrapper = i18n_get_object($type, $i18n_string->objectid, $view);
return $wrapper;
}
// Special handling for i18n_panels.
$panels_objects = array(
'pane_configuration' => 'panels_pane',
'display_configuration' => 'panels_display',
);
if (in_array($type, array_keys($panels_objects))) {
ctools_include('export');
$wrapper = FALSE;
switch ($type) {
case 'display_configuration':
$object_array = ctools_export_load_object($panels_objects[$type], 'conditions', array('uuid' => $i18n_string->objectid));
$wrapper = i18n_get_object($type, $i18n_string->objectid, $object_array[$i18n_string->objectid]);
break;
case 'pane_configuration':
$obj = db_query("SELECT * FROM {panels_pane} WHERE uuid = :uuid", array(':uuid' => $i18n_string->objectid))->fetchObject();
if ($obj) {
$pane = ctools_export_unpack_object($panels_objects[$type], $obj);
$translation = i18n_panels_get_i18n_translation_object($pane);
$translation->uuid = $pane->uuid;
$wrapper = i18n_get_object($type, $i18n_string->objectid, $translation);
}
break;
default:
break;
}
return $wrapper;
}
// Special handling if the object has two keys. Assume that they
// mean type and object id.
if ($type == 'field') {
// Special case for fields which expect the type to be the identifier.
$wrapper = i18n_get_object($type, $i18n_string->type);
return $wrapper;
}
elseif ($type == 'field_instance') {
// Special case for field instances, which use the field name as type and
// bundle as object id. We don't know the entity_type, so we loop over all
// entity_types to search for the bundle. This will clash if different
// entity types have bundles with the same names.
foreach (entity_get_info() as $entity_type => $entity_info) {
if (isset($entity_info['bundles'][$i18n_string->objectid])) {
list($type_key, $objectid_key) = $object_key;
$wrapper = i18n_get_object($type, array(
$type_key => $i18n_string->type,
$objectid_key => $i18n_string->objectid
), field_info_instance($entity_type, $i18n_string->type, $i18n_string->objectid));
return $wrapper;
}
}
}
elseif (count($object_key) == 2) {
list($type_key, $objectid_key) = $object_key;
$wrapper = i18n_get_object($type, array(
$type_key => $i18n_string->type,
$objectid_key => $i18n_string->objectid
));
return $wrapper;
}
else {
// Otherwise, use the object id.
$wrapper = i18n_get_object($type, $i18n_string->objectid);
return $wrapper;
}
}
/**
* Implements hook_tmgmt_source_suggestions()
*/
function tmgmt_i18n_string_tmgmt_source_suggestions(array $items, TMGMTJob $job) {
$suggestions = array();
foreach ($items as $item) {
if (($item instanceof TMGMTJobItem) && ($item->item_type == 'node')) {
// Load translatable menu items related to this node.
$query = db_select('menu_links', 'ml')
->condition('ml.link_path', 'node/' . $item->item_id)
->fields('ml', array('mlid'));
$query->join('menu_custom', 'mc', 'ml.menu_name = mc.menu_name AND mc.i18n_mode = ' . I18N_MODE_MULTIPLE);
$results = $query->execute()->fetchAllAssoc('mlid');
foreach ($results as $result) {
$menu_link = menu_link_load($result->mlid);
// Add suggestion.
$suggestions[] = array(
'job_item' => tmgmt_job_item_create('i18n_string', 'menu_link', "menu:item:{$result->mlid}"),
'reason' => t('Menu link @title', array('@title' => $menu_link['link_title'])),
'from_item' => $item->tjiid,
);
}
}
}
return $suggestions;
}

View File

@@ -0,0 +1,157 @@
<?php
/**
* @file
* Provides the i18n string source controller.
*/
/**
* Translation plugin controller for i18n strings.
*/
class TMGMTI18nStringSourcePluginController extends TMGMTDefaultSourcePluginController {
/**
* {@inheritdoc}
*/
public function getData(TMGMTJobItem $job_item) {
$i18n_object = $this->getI18nObjectWrapper($job_item);
$structure = array();
$languages = language_list();
if ($i18n_object instanceof i18n_string_object_wrapper) {
$i18n_strings = $i18n_object->get_strings();
$source_language = $job_item->getJob()->source_language;
foreach ($i18n_strings as $string_id => $string) {
// If the job source language is different from the i18n source language
// try to load an existing translation for the language and use it as
// the source.
if ($source_language != i18n_string_source_language()) {
$translation = $string->get_translation($source_language);
if (empty($translation)) {
throw new TMGMTException(t('Unable to load %language translation for the string %title',
array('%language' => $languages[$source_language]->name, '%title' => $string->title)));
}
// If '#label' is empty theme_tmgmt_ui_translator_review_form() fails.
$structure[$string_id] = array(
'#label' => !empty($string->title) ? $string->title : $string->property,
'#text' => $translation,
'#translate' => TRUE
);
}
else {
// If '#label' is empty theme_tmgmt_ui_translator_review_form() fails.
$structure[$string_id] = array(
'#label' => !empty($string->title) ? $string->title : $string->property,
'#text' => $string->string,
'#translate' => TRUE
);
}
}
}
return $structure;
}
/**
* {@inheritdoc}
*/
public function saveTranslation(TMGMTJobItem $job_item) {
$job = tmgmt_job_load($job_item->tjid);
$data = array_filter(tmgmt_flatten_data($job_item->getData()), '_tmgmt_filter_data');
foreach ($data as $i18n_string => $item) {
if (isset($item['#translation']['#text'])) {
i18n_string_translation_update($i18n_string, $item['#translation']['#text'], $job->target_language);
}
}
// We just saved the translation, set the state of the job item to
// 'finished'.
$job_item->accepted();
}
/**
* {@inheritdoc}
*/
public function getLabel(TMGMTJobItem $job_item) {
if ($i18n_object = $this->getI18nObjectWrapper($job_item)) {
// Get the label, default to the get_title() method, fall back to the
// first string if that is empty.
$title = t('Unknown');
if ($i18n_object->get_title()) {
$title = $i18n_object->get_title();
}
elseif ($strings = $i18n_object->get_strings(array('empty' => TRUE))) {
$title = reset($strings)->get_string();
}
return t('@title (@id)', array('@title' => strip_tags(drupal_substr($title, 0, 64)), '@id' => $job_item->item_id));
}
return parent::getLabel($job_item);
}
/**
* {@inheritdoc}
*/
public function getUri(TMGMTJobItem $job_item) {
if ($wrapper = $this->getI18nObjectWrapper($job_item)) {
return array(
'path' => $wrapper->get_path(),
'options' => array(),
);
}
}
/**
* {@inheritdoc}
*/
public function getType(TMGMTJobItem $job_item) {
if ($label = $this->getItemTypeLabel($job_item->item_type)) {
return $label;
}
return parent::getType($job_item);
}
/**
* {@inheritdoc}
*/
public function getSourceLangCode(TMGMTJobItem $job_item) {
return i18n_string_source_language();
}
/**
* {@inheritdoc}
*/
public function getExistingLangCodes(TMGMTJobItem $job_item) {
$existing_lang_codes = array();
$languages = language_list();
if ($object = $this->getI18nObjectWrapper($job_item)) {
$existing_lang_codes = array_keys($languages);
foreach ($object->load_strings() as $string) {
foreach ($languages as $language) {
if ($language->language == $this->getSourceLangCode($job_item)) {
continue;
}
// Remove languages for which we fail to find translation.
if (in_array($language->language, $existing_lang_codes) && !$string->get_translation($language->language)) {
$existing_lang_codes = array_diff($existing_lang_codes, array($language->language));
}
}
}
}
return $existing_lang_codes;
}
/**
* Helper function to get i18n_object_wrapper for given job item.
*
* @param TMGMTJobItem $job_item
*
* @return i18n_string_object_wrapper
*/
protected function getI18nObjectWrapper(TMGMTJobItem $job_item) {
list(, $type, $object_id) = explode(':', $job_item->item_id, 3);
return tmgmt_i18n_string_get_wrapper($job_item->item_type, (object) array('type' => $type, 'objectid' => $object_id));
}
}

View File

@@ -0,0 +1,496 @@
<?php
/**
* Basic i18n String Source tests.
*/
class TMGMTI18nStringSourceTestCase extends TMGMTBaseTestCase {
static function getInfo() {
return array(
'name' => 'i18n String Source tests',
'description' => 'Exporting source data from i18n string and saving translations back',
'group' => 'Translation Management',
'dependencies' => array('i18n_string'),
);
}
function setUp() {
parent::setUp(array('tmgmt_ui', 'tmgmt_i18n_string', 'taxonomy', 'i18n_taxonomy', 'i18n_block', 'i18n_field', 'list', 'i18n_menu'));
$this->setEnvironment('de');
$this->translator = $this->createTranslator();
}
function testI18nStringSourceTaxonomy() {
// Test translation of a vocabulary.
/////////////////////////////////////
$config = array(
'name' => $this->randomName(),
'machine_name' => 'test_vocab',
'i18n_mode' => I18N_MODE_LOCALIZE,
);
$vocabulary = entity_create('taxonomy_vocabulary', $config);
taxonomy_vocabulary_save($vocabulary);
$string_object_name = "taxonomy:vocabulary:" . $vocabulary->vid;
$source_text = $vocabulary->name;
// Create the new job and job item.
$job = $this->createJob();
$job->translator = $this->translator->name;
$job->settings = array();
$job->save();
$item1 = $job->addItem('i18n_string', 'taxonomy_vocabulary', $string_object_name);
$this->assertEqual(t('Vocabulary'), $item1->getSourceType());
$job->requestTranslation();
foreach ($job->getItems() as $item) {
/* @var $item TMGMTJobItem */
$item->acceptTranslation();
}
// Check the structure of the imported data.
$this->assertEqual($item1->item_id, $string_object_name, 'i18n Strings object correctly saved');
// Check string translation.
$this->assertEqual(i18n_string_translate('taxonomy:vocabulary:' . $vocabulary->vid . ':name', $source_text, array('langcode' => $job->target_language)), $job->target_language . '_' . $source_text);
// Test translation of a taxonomy term.
/////////////////////////////////////
$term = entity_create('taxonomy_term', array(
'vid' => $vocabulary->vid,
'name' => $this->randomName(),
'description' => $this->randomName(),
));
taxonomy_term_save($term);
$string_object_name = "taxonomy:term:" . $term->tid;
$source_text_name = $term->name;
$source_text_description = $term->description;
// Create the new job and job item.
$job = $this->createJob();
$job->translator = $this->translator->name;
$job->settings = array();
$job->save();
$item1 = $job->addItem('i18n_string', 'taxonomy_term', $string_object_name);
$this->assertEqual(t('Taxonomy term'), $item1->getSourceType());
$job->requestTranslation();
/* @var $item TMGMTJobItem */
foreach ($job->getItems() as $item) {
// The source is available only in en.
$this->assertJobItemLangCodes($item, 'en', array('en'));
$item->acceptTranslation();
// The source should be now available in de and en.
$this->assertJobItemLangCodes($item, 'en', array('de', 'en'));
}
// Check the structure of the imported data.
$this->assertEqual($item1->item_id, $string_object_name);
// Check string translation.
$this->assertEqual(i18n_string_translate('taxonomy:term:' . $term->tid . ':name', $source_text_name,
array('langcode' => $job->target_language)), $job->target_language . '_' . $source_text_name);
$this->assertEqual(i18n_string_translate('taxonomy:term:' . $term->tid . ':description', $source_text_description,
array('langcode' => $job->target_language)), $job->target_language . '_' . $source_text_description);
}
/**
* Test if the source is able to pull content in requested language.
*/
function testRequestDataForSpecificLanguage() {
$this->setEnvironment('es');
$this->setEnvironment('cs');
$config = array(
'name' => $this->randomName(),
'machine_name' => 'test_vocab',
'i18n_mode' => I18N_MODE_LOCALIZE,
);
$vocabulary = entity_create('taxonomy_vocabulary', $config);
taxonomy_vocabulary_save($vocabulary);
$string_object_name = "taxonomy:vocabulary:" . $vocabulary->vid;
i18n_string_translation_update($string_object_name . ':name', 'de translation', 'de');
// Create new job item with a source language for which the translation
// exits.
$job = $this->createJob('de', 'cs');
$job->save();
$job->addItem('i18n_string', 'taxonomy_vocabulary', $string_object_name);
$data = $job->getData();
$this->assertEqual($data[1][$string_object_name . ':name']['#text'], 'de translation');
// Create new job item with a source language for which the translation
// does not exit.
$job = $this->createJob('es', 'cs');
$job->save();
try {
$job->addItem('i18n_string', 'taxonomy_vocabulary', $string_object_name);
$this->fail('The job item should not be added as there is no translation for language "es"');
}
catch (TMGMTException $e) {
$languages = language_list();
$this->assertEqual(t('Unable to load %language translation for the string %title',
array('%language' => $languages['es']->name, '%title' => 'Name')), $e->getMessage());
}
}
function testI18nStringSourceMenu() {
drupal_static_reset('_tmgmt_plugin_info');
drupal_static_reset('_tmgmt_plugin_controller');
// Test translation of a menu.
/////////////////////////////////////
$config = array(
'menu_name' => $this->randomName(),
'title' => $this->randomName(),
'description' => $this->randomName(),
'i18n_mode' => I18N_MODE_MULTIPLE,
);
menu_save($config);
$menu = menu_load($config['menu_name']);
$source_text = $menu['title'];
$string_name = 'menu:menu:' . $menu['menu_name'];
// Create the new job and job item.
$job = $this->createJob();
$job->translator = $this->translator->name;
$job->settings = array();
$item1 = $job->addItem('i18n_string', 'menu', $string_name);
$this->assertEqual(t('Menu'), $item1->getSourceType());
$job->requestTranslation();
/* @var $item TMGMTJobItem */
foreach ($job->getItems() as $item) {
$this->assertJobItemLangCodes($item, 'en', array('en'));
$item->acceptTranslation();
$this->assertJobItemLangCodes($item, 'en', array('de', 'en'));
}
$data = $item1->getData();
$this->assertEqual($data['menu:menu:' . $menu['menu_name'] . ':title']['#text'], $config['title']);
$this->assertEqual($data['menu:menu:' . $menu['menu_name'] . ':description']['#text'], $config['description']);
// Check the structure of the imported data.
$this->assertEqual($item1->item_id, $string_name, 'String is correctly saved');
// Check string translation.
$this->assertEqual(i18n_string_translate($string_name . ':title', $source_text, array('langcode' => $job->target_language)), $job->target_language . '_' . $source_text);
// Test translation of a menu item.
/////////////////////////////////////
$source_text = $this->randomName();
$menu_link = array(
'link_path' => '<front>',
'link_title' => $source_text,
// i18n_menu_link::get_title() uses the title, set that too.
'title' => $source_text,
'menu_name' => $menu['menu_name'],
'customized' => TRUE,
);
menu_link_save($menu_link);
$string_name = 'menu:item:' . $menu_link['mlid'];
// Create the new job and job item.
$job = $this->createJob();
$job->translator = $this->translator->name;
$job->settings = array();
$item1 = $job->addItem('i18n_string', 'menu_link', $string_name);
$this->assertEqual(t('Menu link'), $item1->getSourceType());
$job->requestTranslation();
/* @var $item TMGMTJobItem */
foreach ($job->getItems() as $item) {
$this->assertJobItemLangCodes($item, 'en', array('en'));
$item->acceptTranslation();
$this->assertJobItemLangCodes($item, 'en', array('de', 'en'));
}
$data = $item1->getData();
$this->assertEqual($data[$string_name . ':title']['#text'], $source_text);
// Check the structure of the imported data.
$this->assertEqual($item1->item_id, $string_name);
// Check string translation.
$this->assertEqual(i18n_string_translate($string_name . ':title', $source_text, array('langcode' => $job->target_language)), $job->target_language . '_' . $source_text);
}
function testI18nStringSourceLangCodes() {
$config = array(
'name' => $this->randomName(),
'description' => 'description_' . $this->randomName(),
'machine_name' => 'test_vocab',
'i18n_mode' => I18N_MODE_LOCALIZE,
);
$vocabulary = entity_create('taxonomy_vocabulary', $config);
taxonomy_vocabulary_save($vocabulary);
$string_object_name = "taxonomy:vocabulary:" . $vocabulary->vid;
// Create the new job and job item.
$job = $this->createJob();
$job->translator = $this->translator->name;
$job->settings = array();
$job->save();
$item = $job->addItem('i18n_string', 'taxonomy_vocabulary', $string_object_name);
$this->assertJobItemLangCodes($item, 'en', array('en'));
i18n_string_translation_update($string_object_name . ':description', 'de_' . $config['description'], 'de');
$this->assertJobItemLangCodes($item, 'en', array('en'));
i18n_string_translation_update($string_object_name . ':name', 'de_' . $config['name'], 'de');
$this->assertJobItemLangCodes($item, 'en', array('en', 'de'));
}
function testI18nStringPluginUI() {
$this->loginAsAdmin(array('administer taxonomy', 'translate interface', 'translate user-defined strings'));
$vocab_data = array(
'name' => $this->randomName(),
'machine_name' => 'test_vocab',
'i18n_mode' => I18N_MODE_LOCALIZE,
);
$term_data = array(
'name' => $this->randomName(),
);
$vocab_data_not_translated = array(
'name' => $this->randomName(),
'machine_name' => 'test_vocab3',
'i18n_mode' => I18N_MODE_LOCALIZE,
);
// Configure taxonomy and create vocab + term.
$this->drupalPost('admin/structure/taxonomy/add', $vocab_data, t('Save'));
$this->drupalGet('admin/structure/taxonomy');
$this->clickLink(t('add terms'));
$this->drupalPost(NULL, $term_data, t('Save'));
$this->drupalPost('admin/structure/taxonomy/add', $vocab_data_not_translated, t('Save'));
$this->drupalGet('admin/tmgmt/sources/i18n_string_taxonomy_vocabulary');
$this->assertText($vocab_data['name']);
// Request translation via i18n source tab
$this->drupalPost(NULL, array('items[taxonomy:vocabulary:1]' => 1), t('Request translation'));
// Test for the job checkout url.
$this->assertTrue(strpos($this->getUrl(), 'admin/tmgmt/jobs') !== FALSE);
entity_get_controller('tmgmt_job')->resetCache();
$jobs = entity_load('tmgmt_job', FALSE);
/** @var TMGMTJob $job */
$job = array_pop($jobs);
$this->assertFieldByName('label', $job->label());
// Request translation via translate tab of i18n.
$this->drupalPost('admin/structure/taxonomy/test_vocab/translate', array('languages[taxonomy:vocabulary:1:de]' => 1), t('Request translation'));
$this->drupalPost(NULL, array(), t('Submit to translator'));
// Verify that the job item status is shown.
$this->assertText(t('Needs review'));
$this->clickLink(t('review'));
$this->drupalPost(NULL, array(), t('Save as completed'));
$this->assertText(t('The translation for @label has been accepted.', array('@label' => $job->label())));
// Test the missing translation filter.
$this->drupalGet('admin/tmgmt/sources/i18n_string_taxonomy_vocabulary');
// Check that the source language has been removed from the target language
// select box.
$elements = $this->xpath('//select[@name=:name]//option[@value=:option]', array(':name' => 'search[target_language]', ':option' => i18n_string_source_language()));
$this->assertTrue(empty($elements));
$edit = array(
'search[target_language]' => 'de',
'search[target_status]' => 'untranslated',
);
$this->drupalPost('admin/tmgmt/sources/i18n_string_taxonomy_vocabulary', $edit, t('Search'));
// The vocabulary name is translated to "de" therefore it must not show up
// in the list.
$this->assertNoText($vocab_data['name']);
$this->assertText($vocab_data_not_translated['name']);
$edit = array(
'search[target_language]' => 'de',
'search[target_status]' => 'untranslated',
);
$this->drupalPost(NULL, $edit, t('Search'));
$this->assertNoText($vocab_data['name']);
$this->assertText($vocab_data_not_translated['name']);
// Update the string status to I18N_STRING_STATUS_UPDATE.
$lid = db_select('locales_source', 's')->fields('s', array('lid'))->condition('source', $vocab_data['name'])->execute()->fetchField();
db_update('locales_target')->fields(array('i18n_status' => I18N_STRING_STATUS_UPDATE))->condition('lid', $lid)->execute();
$edit = array(
'search[target_language]' => 'de',
'search[target_status]' => 'outdated',
);
$this->drupalPost(NULL, $edit, t('Search'));
$this->assertText($vocab_data['name']);
$this->assertNoText($vocab_data_not_translated['name']);
$edit = array(
'search[target_language]' => 'de',
'search[target_status]' => 'untranslated_or_outdated',
);
$this->drupalPost(NULL, $edit, t('Search'));
$this->assertText($vocab_data['name']);
$this->assertText($vocab_data_not_translated['name']);
}
/**
* Tests translation of blocks through the user interface.
*/
function testI18nStringPluginUIBlock() {
$this->loginAsAdmin(array('administer blocks', 'translate interface', 'translate user-defined strings'));
// Make some blocks translatable.
$navigation_edit = array(
'title' => $this->randomName(),
'i18n_mode' => 1,
);
$this->drupalPost('admin/structure/block/manage/system/navigation/configure', $navigation_edit, t('Save block'));
$powered_edit = array(
'title' => $this->randomName(),
'i18n_mode' => 1,
);
$this->drupalPost('admin/structure/block/manage/system/powered-by/configure', $powered_edit, t('Save block'));
$this->drupalGet('admin/tmgmt/sources/i18n_string_block');
$this->assertText($navigation_edit['title']);
$this->assertText($powered_edit['title']);
// Request translation via i18n source tab.
$edit = array(
'items[blocks:system:powered-by]' => 1,
'items[blocks:system:navigation]' => 1,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
$this->assertText($navigation_edit['title']);
$this->assertText($powered_edit['title']);
$this->drupalPost(NULL, array(), t('Submit to translator'));
$this->assertRaw(t('Active job item: Needs review'));
}
/**
* Tests translation of fields through the user interface.
*/
function testI18nStringPluginUIField() {
$this->loginAsAdmin(array('translate interface', 'translate user-defined strings'));
$type = $this->drupalCreateContentType(array('type' => $type = $this->randomName()));
// Create a field.
$field = array(
'field_name' => 'list_test',
'type' => 'list_text',
);
for ($i = 0; $i < 5; $i++) {
$field['settings']['allowed_values'][$this->randomName()] = $this->randomString();
}
field_create_field($field);
// Create an instance of the previously created field.
$instance = array(
'field_name' => 'list_test',
'entity_type' => 'node',
'bundle' => $type->type,
'label' => $this->randomName(10),
'description' => $this->randomString(30),
);
field_create_instance($instance);
// The body field doesn't have anything that can be translated on the field
// level, so it shouldn't show up in the field overview.
$this->drupalGet('admin/tmgmt/sources/i18n_string_field');
$this->assertNoText(t('Body'));
// @todo: Label doesn't work here?
$this->assertText('field:list_test:#allowed_values');
$this->drupalGet('admin/tmgmt/sources/i18n_string_field_instance');
$this->assertUniqueText(t('Body'));
$this->assertUniqueText($instance['label']);
// Request translation.
$edit = array(
'items[field:body:' . $type->type . ']' => 1,
'items[field:list_test:' . $type->type . ']' => 1,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
$this->assertText(t('Body'));
$this->assertText($instance['label']);
$this->drupalPost(NULL, array(), t('Submit to translator'));
$this->assertRaw(t('Active job item: Needs review'));
// Review the first item.
$this->clickLink(t('reviewed'));
$this->drupalPost(NULL, array(), t('Save as completed'));
// The overview should now have a translated field and a pending job item.
$this->drupalGet('admin/tmgmt/sources/i18n_string_field_instance');
$this->assertRaw(t('Translation up to date'));
$this->assertRaw(t('Active job item: Needs review'));
}
/**
* Test the i18n specific cart functionality.
*/
function testCart() {
$vocabulary1 = entity_create('taxonomy_vocabulary', array(
'name' => $this->randomName(),
'description' => 'description_' . $this->randomName(),
'machine_name' => 'test_vocab1',
'i18n_mode' => I18N_MODE_LOCALIZE,
));
taxonomy_vocabulary_save($vocabulary1);
$string1 = "taxonomy:vocabulary:" . $vocabulary1->vid;
$vocabulary2 = entity_create('taxonomy_vocabulary', array(
'name' => $this->randomName(),
'description' => 'description_' . $this->randomName(),
'machine_name' => 'test_vocab2',
'i18n_mode' => I18N_MODE_LOCALIZE,
));
taxonomy_vocabulary_save($vocabulary2);
$string2 = "taxonomy:vocabulary:" . $vocabulary2->vid;
$vocabulary3 = entity_create('taxonomy_vocabulary', array(
'name' => $this->randomName(),
'description' => 'description_' . $this->randomName(),
'machine_name' => 'test_vocab3',
'i18n_mode' => I18N_MODE_LOCALIZE,
));
taxonomy_vocabulary_save($vocabulary3);
$this->loginAsAdmin(array_merge($this->translator_permissions, array('translate interface', 'translate user-defined strings')));
// Test source overview.
$this->drupalPost('admin/tmgmt/sources/i18n_string_taxonomy_vocabulary', array(
'items[' . $string1 . ']' => TRUE,
'items[' . $string2 . ']' => TRUE,
), t('Add to cart'));
$this->drupalGet('admin/tmgmt/cart');
$this->assertText($vocabulary1->name);
$this->assertText($vocabulary2->name);
// Test translate tab.
$this->drupalGet('admin/structure/taxonomy/test_vocab3/translate');
$this->assertRaw(t('There are @count items in the <a href="@url">translation cart</a>.',
array('@count' => 2, '@url' => url('admin/tmgmt/cart'))));
$this->drupalPost(NULL, array(), t('Add to cart'));
$this->assertRaw(t('@count content source was added into the <a href="@url">cart</a>.', array('@count' => 1, '@url' => url('admin/tmgmt/cart'))));
$this->assertRaw(t('There are @count items in the <a href="@url">translation cart</a> including the current item.',
array('@count' => 3, '@url' => url('admin/tmgmt/cart'))));
}
}

View File

@@ -0,0 +1,295 @@
<?php
/**
* @file
* Provides the I18nString source controller.
*/
/**
* Class TMGMTI18nStringDefaultSourceUIController
*
* UI Controller fo i18n strings translation jobs.
*/
class TMGMTI18nStringDefaultSourceUIController extends TMGMTDefaultSourceUIController {
/**
* Gets overview form header.
*
* @return array
* Header array definition as expected by theme_tablesort().
*/
public function overviewFormHeader() {
$languages = array();
foreach (language_list() as $langcode => $language) {
$langcode = str_replace('-', '', $langcode);
$languages['langcode-' . $langcode] = array(
'data' => check_plain($language->name),
);
}
$header = array(
'title' => array('data' => t('Label (in source language)')),
'type' => array('data' => t('Type')),
) + $languages;
return $header;
}
/**
* {@inheritdoc}
*/
public function overviewForm($form, &$form_state, $type) {
$form += $this->overviewSearchFormPart($form, $form_state, $type);
$form['items'] = array(
'#type' => 'tableselect',
'#header' => $this->overviewFormHeader($type),
'#empty' => t('No strings matching given criteria have been found.')
);
$search_data = $this->getSearchFormSubmittedParams();
$i18n_strings = tmgmt_i18n_string_get_strings($type, $search_data['label'], $search_data['target_language'], $search_data['target_status']);
foreach ($this->getTranslationData($i18n_strings, $form_state['item_type']) as $id => $data) {
$form['items']['#options'][$id] = $this->overviewRow($type, $data);
}
$form['pager'] = array('#markup' => theme('pager', array('tags' => NULL)));
return $form;
}
/**
* Helper function to create translation data list for the sources page list.
*
* @param array $i18n_strings
* Result of the search query returned by tmgmt_i18n_string_get_strings().
* @param string $type
* I18n object type.
*
* @return array
* Structured array with translation data.
*/
protected function getTranslationData($i18n_strings, $type) {
$objects = array();
$source_language = variable_get_value('i18n_string_source_language');
foreach ($i18n_strings as $i18n_string) {
$wrapper = tmgmt_i18n_string_get_wrapper($type, $i18n_string);
if ($wrapper instanceof i18n_string_object_wrapper) {
$id = $i18n_string->job_item_id;
// Get existing translations and current job items for the entity
// to determine translation statuses
$current_job_items = tmgmt_job_item_load_latest('i18n_string', $wrapper->get_type(), $id, $source_language);
$objects[$id] = array(
'id' => $id,
'object' => $wrapper->get_strings(array('empty' => TRUE)),
'wrapper' => $wrapper,
);
// Load entity translation specific data.
foreach (language_list() as $langcode => $language) {
$langcode = str_replace('-', '', $langcode);
$translation_status = 'current';
if ($langcode == $source_language) {
$translation_status = 'original';
}
elseif ($i18n_string->{'lang_' . $langcode} === NULL) {
$translation_status = 'missing';
}
$objects[$id]['current_job_items'][$langcode] = isset($current_job_items[$langcode]) ? $current_job_items[$langcode] : NULL;
$objects[$id]['translation_statuses'][$langcode] = $translation_status;
}
}
}
return $objects;
}
/**
* Builds search form for entity sources overview.
*
* @param array $form
* Drupal form array.
* @param $form_state
* Drupal form_state array.
* @param $type
* Entity type.
*
* @return array
* Drupal form array.
*/
public function overviewSearchFormPart($form, &$form_state, $type) {
$options = array();
foreach (language_list() as $langcode => $language) {
$options[$langcode] = $language->name;
}
$default_values = $this->getSearchFormSubmittedParams();
$form['search_wrapper'] = array(
'#prefix' => '<div class="tmgmt-sources-wrapper tmgmt-i18n_string-sources-wrapper">',
'#suffix' => '</div>',
'#weight' => -15,
);
$form['search_wrapper']['search'] = array(
'#tree' => TRUE,
);
$form['search_wrapper']['search']['label'] = array(
'#type' => 'textfield',
'#title' => t('Label in source language'),
'#default_value' => isset($default_values['label']) ? $default_values['label'] : NULL,
);
// Unset the source language as it should not be listed among target
// languages.
unset($options[i18n_string_source_language()]);
$form['search_wrapper']['search']['target_language'] = array(
'#type' => 'select',
'#title' => t('Target language'),
'#options' => $options,
'#empty_option' => t('Any'),
'#default_value' => isset($default_values['target_language']) ? $default_values['target_language'] : NULL,
);
$form['search_wrapper']['search']['target_status'] = array(
'#type' => 'select',
'#title' => t('Target status'),
'#options' => array(
'untranslated_or_outdated' => t('Untranslated or outdated'),
'untranslated' => t('Untranslated'),
'outdated' => t('Outdated'),
),
'#default_value' => isset($default_values['target_status']) ? $default_values['target_status'] : NULL,
'#states' => array(
'invisible' => array(
':input[name="search[target_language]"]' => array('value' => ''),
),
),
);
$form['search_wrapper']['search_submit'] = array(
'#type' => 'submit',
'#value' => t('Search'),
);
return $form;
}
/**
* Gets submitted search params.
*
* @return array
*/
public function getSearchFormSubmittedParams() {
$params = array(
'label' => NULL,
'target_language' => NULL,
'target_status' => NULL,
);
if (isset($_GET['label'])) {
$params['label'] = $_GET['label'];
}
if (isset($_GET['target_language'])) {
$params['target_language'] = $_GET['target_language'];
}
if (isset($_GET['target_status'])) {
$params['target_status'] = $_GET['target_status'];
}
return $params;
}
/**
* Builds a table row for overview form.
*
* @param string $type
* i18n type.
* @param array $data
* Data needed to build the list row.
*
* @return array
*/
public function overviewRow($type, $data) {
// Set the default item key, assume it's the first.
$item_title = reset($data['object']);
$type_label = i18n_object_info($type, 'title');
$row = array(
'id' => $data['id'],
'title' => $item_title->get_string() ? t('@title (@id)', array('@title' => $item_title->get_string(), '@id' => $data['id'])) : $data['id'],
'type' => empty($type_label) ? t('Unknown') : $type_label,
);
foreach (language_list() as $langcode => $language) {
$langcode = str_replace('-', '', $langcode);
$row['langcode-' . $langcode] = theme('tmgmt_ui_translation_language_status_single', array(
'translation_status' => $data['translation_statuses'][$langcode],
'job_item' => isset($data['current_job_items'][$langcode]) ? $data['current_job_items'][$langcode] : NULL,
));
}
return $row;
}
/**
* {@inheritdoc}
*/
public function overviewFormSubmit($form, &$form_state, $type) {
// Handle search redirect.
$this->overviewSearchFormRedirect($form, $form_state, $type);
$items = array_filter($form_state['values']['items']);
$type = $form_state['item_type'];
$source_lang = variable_get_value('i18n_string_source_language');
// Create only single job for all items as the source language is just
// the same for all.
$job = tmgmt_job_create($source_lang, NULL, $GLOBALS['user']->uid);
// Loop through entities and create individual jobs for each source language.
foreach ($items as $item) {
$job->addItem('i18n_string', $type, $item);
}
$form_state['redirect'] = array('admin/tmgmt/jobs/' . $job->tjid,
array('query' => array('destination' => current_path())));
drupal_set_message(t('One job needs to be checked out.'));
}
/**
* Performs redirect with search params appended to the uri.
*
* In case of triggering element is edit-search-submit it redirects to
* current location with added query string containing submitted search form
* values.
*
* @param array $form
* Drupal form array.
* @param $form_state
* Drupal form_state array.
* @param $type
* Entity type.
*/
public function overviewSearchFormRedirect($form, &$form_state, $type) {
if ($form_state['triggering_element']['#id'] == 'edit-search-submit') {
$query = array();
foreach ($form_state['values']['search'] as $key => $value) {
$query[$key] = $value;
}
drupal_goto($_GET['q'], array('query' => $query));
}
}
}

View File

@@ -0,0 +1,13 @@
msgid ""
msgstr ""
"Project-Id-Version: Drupal 7\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
msgid "Hello World"
msgstr "Hallo Welt"
msgid "Example"
msgstr "Beispiel"

View File

@@ -0,0 +1,17 @@
name = Locales Source
description = Locales source plugin for the Translation Management system.
package = Translation Management
core = 7.x
dependencies[] = tmgmt
dependencies[] = locale
files[] = tmgmt_locale.plugin.inc
files[] = tmgmt_locale.test
files[] = tmgmt_locale.ui.inc
files[] = tmgmt_locale.ui.test
; Information added by Drupal.org packaging script on 2016-09-21
version = "7.x-1.0-rc2+1-dev"
core = "7.x"
project = "tmgmt"
datestamp = "1474446494"

View File

@@ -0,0 +1,28 @@
<?php
/**
* @file
* Installation hooks for tmgmt_locale module.
*/
/**
* Update existing {locales_target}.l10n_status if any.
*/
function tmgmt_locale_update_7000() {
if (module_exists('l10n_update')) {
module_load_include('inc', 'l10n_update');
$query = db_select('tmgmt_job_item', 'ji')
->condition('ji.plugin', 'locale')
->condition('ji.state', TMGMT_JOB_ITEM_STATE_ACCEPTED);
$query->innerJoin('tmgmt_job', 'j', 'j.tjid = ji.tjid');
$query->addField('ji', 'item_id', 'lid');
$query->addField('j', 'target_language', 'language');
foreach ($query->execute() as $row) {
db_update('locales_target')
->condition('lid', $row->lid)
->condition('language', $row->language)
->fields(array('l10n_status' => L10N_UPDATE_STRING_CUSTOM))
->execute();
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* @file
* Source plugin for the Translation Management system that handles locale strings.
*/
/**
* Implements hook_tmgmt_source_plugin_info().
*
* @see TMGMTLocaleSourcePluginController
*/
function tmgmt_locale_tmgmt_source_plugin_info() {
$info['locale'] = array(
'label' => t('Locale source'),
'description' => t('Source handler for locale strings.'),
'plugin controller class' => 'TMGMTLocaleSourcePluginController',
'ui controller class' => 'TMGMTLocaleSourceUIController',
'item types' => array(
'default' => t('Locale'),
),
);
return $info;
}

View File

@@ -0,0 +1,245 @@
<?php
/**
* @file
* Provides the locale source controller.
*/
/**
* Translation plugin controller for locale strings.
*/
class TMGMTLocaleSourcePluginController extends TMGMTDefaultSourcePluginController {
/**
* Updates translation associated to a specific locale source.
*
* @param string $lid
* The Locale ID.
* @param string $target_language
* Target language to update translation.
* @param string $translation
* Translation value.
*
* @return bool
* Success or not updating the locale translation.
*/
protected function updateTranslation($lid, $target_language, $translation) {
$languages = locale_language_list('name', TRUE);
if (!$lid || !array_key_exists($target_language, $languages) || !$translation) {
return FALSE;
}
$exists = db_query("SELECT COUNT(lid) FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $target_language))
->fetchField();
$fields = array(
'translation' => $translation,
);
if (module_exists('l10n_update')) {
module_load_include('inc', 'l10n_update');
$fields += array(
'l10n_status' => L10N_UPDATE_STRING_CUSTOM,
);
}
// @todo Only singular strings are managed here, we should take care of
// plural information of processed string.
if (!$exists) {
$fields += array(
'lid' => $lid,
'language' => $target_language,
);
db_insert('locales_target')
->fields($fields)
->execute();
}
else {
db_update('locales_target')
->fields($fields)
->condition('lid', $lid)
->condition('language', $target_language)
->execute();
}
// Clear locale caches.
_locale_invalidate_js($target_language);
cache_clear_all('locale:' . $target_language, 'cache');
return TRUE;
}
/**
* Helper function to obtain a locale object for given job item.
*
* @param TMGMTJobItem $job_item
*
* @return locale object
*/
protected function getLocaleObject(TMGMTJobItem $job_item) {
$locale_lid = $job_item->item_id;
// Check existence of assigned lid.
$exists = db_query("SELECT COUNT(lid) FROM {locales_source} WHERE lid = :lid", array(':lid' => $locale_lid))->fetchField();
if (!$exists) {
throw new TMGMTException(t('Unable to load locale with id %id', array('%id' => $job_item->item_id)));
}
// This is necessary as the method is also used in the getLabel() callback
// and for that case the job is not available in the cart.
if (!empty($job_item->tjid)) {
$source_language = $job_item->getJob()->source_language;
}
else {
$source_language = $job_item->getSourceLangCode();
}
if ($source_language == 'en') {
$query = db_select('locales_source', 'ls');
$query
->fields('ls')
->condition('ls.lid', $locale_lid);
$locale_object = $query
->execute()
->fetchObject();
$locale_object->language = 'en';
if (empty($locale_object)) {
return null;
}
$locale_object->origin = 'source';
}
else {
$query = db_select('locales_target', 'lt');
$query->join('locales_source', 'ls', 'ls.lid = lt.lid');
$query
->fields('lt')
->fields('ls')
->condition('lt.lid', $locale_lid)
->condition('lt.language', $source_language);
$locale_object = $query
->execute()
->fetchObject();
if (empty($locale_object)) {
return null;
}
$locale_object->origin = 'target';
}
return $locale_object;
}
/**
* {@inheritdoc}
*/
public function getLabel(TMGMTJobItem $job_item) {
if ($locale_object = $this->getLocaleObject($job_item)) {
if ($locale_object->origin == 'source') {
$label = $locale_object->source;
}
else {
$label = $locale_object->translation;
}
return truncate_utf8(strip_tags($label), 30, FALSE, TRUE);
}
}
/**
* [@inheritdoc}
*/
public function getType(TMGMTJobItem $job_item) {
return $this->getItemTypeLabel($job_item->item_type);
}
/**
* {@inheritdoc}
*/
public function getData(TMGMTJobItem $job_item) {
$locale_object = $this->getLocaleObject($job_item);
if (empty($locale_object)) {
$languages = language_list();
throw new TMGMTException(t('Unable to load %language translation for the locale %id',
array('%language' => $languages[$job_item->getJob()->source_language]->name, '%id' => $job_item->item_id)));
}
if ($locale_object->origin == 'source') {
$text = $locale_object->source;
}
else {
$text = $locale_object->translation;
}
// Identify placeholders that need to be escaped. Assume that placeholders
// consist of alphanumeric characters and _,- only and are delimited by
// non-alphanumeric characters. There are cases that don't match, for
// example appended SI units like "@valuems", there only @value is the
// actual placeholder.
$escape = array();
if (preg_match_all('/([@!%][a-zA-Z0-9_-]+)/', $text, $matches, PREG_OFFSET_CAPTURE)) {
foreach ($matches[0] as $match) {
$escape[$match[1]]['string'] = $match[0];
}
}
$structure['singular'] = array(
'#label' => t('Singular'),
'#text' => (string) $text,
'#translate' => TRUE,
'#escape' => $escape,
);
return $structure;
}
/**
* {@inheritdoc}
*/
public function saveTranslation(TMGMTJobItem $job_item) {
$job = tmgmt_job_load($job_item->tjid);
$data = $job_item->getData();
if (isset($data['singular'])) {
$translation = $data['singular']['#translation']['#text'];
// Update the locale string in the system.
// @todo: Send error message to user if update fails.
if ($this->updateTranslation($job_item->item_id, $job->target_language, $translation)) {
$job_item->accepted();
}
}
// @todo: Temporary backwards compability with existing jobs, remove in next
// release.
if (isset($data[$job_item->item_id])) {
$translation = $data[$job_item->item_id]['#translation']['#text'];
// Update the locale string in the system.
// @todo: Send error message to user if update fails.
if ($this->updateTranslation($job_item->item_id, $job->target_language, $translation)) {
$job_item->accepted();
}
}
}
/**
* {@inheritdoc}
*/
public function getSourceLangCode(TMGMTJobItem $job_item) {
// For the locale source English is always the source language.
return 'en';
}
/**
* {@inheritdoc}
*/
public function getExistingLangCodes(TMGMTJobItem $job_item) {
$query = db_select('locales_target', 'lt');
$query->fields('lt', array('language'));
$query->condition('lt.lid', $job_item->item_id);
$existing_lang_codes = array('en');
foreach ($query->execute() as $language) {
$existing_lang_codes[] = $language->language;
}
return $existing_lang_codes;
}
}

View File

@@ -0,0 +1,231 @@
<?php
/**
* Basic Locale Source tests.
*/
class TMGMTLocaleSourceTestCase extends TMGMTBaseTestCase {
static function getInfo() {
return array(
'name' => 'Locale Source tests',
'description' => 'Exporting source data from locale and saving translations back',
'group' => 'Translation Management',
);
}
function setUp() {
parent::setUp(array('tmgmt_locale'));
$this->langcode = 'de';
$this->context = 'default';
$file = new stdClass();
$file->uri = drupal_realpath(drupal_get_path('module', 'tmgmt_locale') . '/tests/test.xx.po');
$this->pofile = file_save($file);
$this->setEnvironment($this->langcode);
$this->setEnvironment('es');
}
/**
* Tests translation of a locale singular term.
*/
function testSingularTerm() {
// Load PO file to create a locale structure in the database.
_locale_import_po($this->pofile, $this->langcode, LOCALE_IMPORT_OVERWRITE, $this->context);
// Obtain one locale string with translation.
$locale_object = db_query('SELECT * FROM {locales_source} WHERE source = :source LIMIT 1', array(':source' => 'Hello World'))->fetchObject();
$source_text = $locale_object->source;
// Create the new job and job item.
$job = $this->createJob();
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
$item1 = $job->addItem('locale', 'default', $locale_object->lid);
// Check the structure of the imported data.
$this->assertEqual($item1->item_id, $locale_object->lid, 'Locale Strings object correctly saved');
$this->assertEqual('Locale', $item1->getSourceType());
$this->assertEqual('Hello World', $item1->getSourceLabel());
$job->requestTranslation();
foreach ($job->getItems() as $item) {
/* @var $item TMGMTJobItem */
$item->acceptTranslation();
$this->assertTrue($item->isAccepted());
// The source is now available in en and de.
$this->assertJobItemLangCodes($item, 'en', array('en', 'de'));
}
// Check string translation.
$expected_translation = $job->target_language . '_' . $source_text;
$this->assertTranslation($locale_object->lid, 'de', $expected_translation);
// Translate the german translation to spanish.
$target_langcode = 'es';
$job = $this->createJob('de', $target_langcode);
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
$item1 = $job->addItem('locale', 'default', $locale_object->lid);
$this->assertEqual('Locale', $item1->getSourceType());
$this->assertEqual($expected_translation, $item1->getSourceLabel());
$job->requestTranslation();
foreach ($job->getItems() as $item) {
/* @var $item TMGMTJobItem */
$item->acceptTranslation();
$this->assertTrue($item->isAccepted());
// The source should be now available for en, de and es languages.
$this->assertJobItemLangCodes($item, 'en', array('en', 'de', 'es'));
}
// Check string translation.
$this->assertTranslation($locale_object->lid, $target_langcode, $job->target_language . '_' . $expected_translation);
}
/**
* Test if the source is able to pull content in requested language.
*/
function testRequestDataForSpecificLanguage() {
$this->setEnvironment('cs');
_locale_import_po($this->pofile, $this->langcode, LOCALE_IMPORT_OVERWRITE, $this->context);
$locale_object = db_query('SELECT * FROM {locales_source} WHERE source = :source LIMIT 1', array(':source' => 'Hello World'))->fetchObject();
$plugin = new TMGMTLocaleSourcePluginController('locale', 'locale');
$reflection_plugin = new ReflectionClass('TMGMTLocaleSourcePluginController');
$updateTranslation = $reflection_plugin->getMethod('updateTranslation');
$updateTranslation->setAccessible(TRUE);
$updateTranslation->invoke($plugin, $locale_object->lid, 'de', 'de translation');
// Create the new job and job item.
$job = $this->createJob('de', 'cs');
$job->save();
$job->addItem('locale', 'default', $locale_object->lid);
$data = $job->getData();
$this->assertEqual($data[1]['singular']['#text'], 'de translation');
// Create new job item with a source language for which the translation
// does not exit.
$job = $this->createJob('es', 'cs');
$job->save();
try {
$job->addItem('locale', 'default', $locale_object->lid);
$this->fail('The job item should not be added as there is no translation for language "es"');
}
catch (TMGMTException $e) {
$languages = language_list();
$this->assertEqual(t('Unable to load %language translation for the locale %id',
array('%language' => $languages['es']->name, '%id' => $locale_object->lid)), $e->getMessage());
}
}
/**
* Verifies that strings that need escaping are correctly identified.
*/
function testEscaping() {
$lid = db_insert('locales_source')
->fields(array(
'source' => '@place-holders need %to be !esc_aped.',
'textgroup' => 'default',
'context' => '',
))
->execute();
$job = $this->createJob('en', 'de');
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
$item = $job->addItem('locale', 'default', $lid);
$data = $item->getData();
$expected_escape = array(
0 => array('string' => '@place-holders'),
20 => array('string' => '%to'),
27 => array('string' => '!esc_aped'),
);
$this->assertEqual($data['singular']['#escape'], $expected_escape);
// Invalid patterns that should be ignored.
$lid = db_insert('locales_source')
->fields(array(
'source' => '@ % ! example',
'textgroup' => 'default',
'context' => '',
))
->execute();
$item = $job->addItem('locale', 'default', $lid);
$data = $item->getData();
$this->assertTrue(empty($data[$lid]['#escape']));
}
/**
* Tests that system behaves correctly with an non-existing locales.
*/
function testInexistantSource() {
// Create inexistant locale object.
$locale_object = new stdClass();
$locale_object->lid = 0;
// Create the job.
$job = $this->createJob();
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
// Create the job item.
try {
$job->addItem('locale', 'default', $locale_object->lid);
$this->fail('Job item add with an inexistant locale.');
}
catch (TMGMTException $e) {
$this->pass('Exception thrown when trying to translate non-existing locale string');
}
// Try to translate a source string without translation from german to
// spanish.
$lid = db_insert('locales_source')
->fields(array(
'source' => 'No translation',
'textgroup' => 'default',
'context' => '',
))
->execute();
$job = $this->createJob('de', 'fr');
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
try {
$job->addItem('locale', 'default', $lid);
$this->fail('Job item add with an non-existing locale did not fail.');
}
catch (TMGMTException $e) {
$this->pass('Job item add with an non-existing locale did fail.');
}
}
/**
* Asserts a locale translation.
*
* @param int $lid
* The locale source id.
* @param string $target_langcode
* The target language code.
* @param string $expected_translation
* The expected translation.
*/
public function assertTranslation($lid, $target_langcode, $expected_translation) {
$actual_translation = db_query('SELECT translation FROM {locales_target} WHERE lid = :lid AND language = :language', array(
':lid' => $lid,
':language' => $target_langcode
))->fetchField();
$this->assertEqual($actual_translation, $expected_translation);
}
}

View File

@@ -0,0 +1,316 @@
<?php
/**
* @file
* Provides the I18nString source controller.
*/
/**
* Class TMGMTI18nStringDefaultSourceUIController
*
* UI Controller fo i18n strings translation jobs.
*/
class TMGMTLocaleSourceUIController extends TMGMTDefaultSourceUIController {
/**
* Gets locale strings.
*
* @param string $textgroup
* The locale textgroup.
* @param string $search_label
* Label to search for.
* @param string $missing_target_language
* Missing translation language.
*
* @return array
* List of i18n strings data.
*/
function getStrings($textgroup, $search_label = NULL, $missing_target_language = NULL) {
$languages = drupal_map_assoc(array_keys(language_list()));
$select = db_select('locales_source', 'ls')
->fields('ls', array('lid', 'source'));
$select->addTag('tmgmt_sources_search');
$select->addMetaData('plugin', 'locale');
$select->addMetaData('type', $textgroup);
$select->condition('ls.textgroup', $textgroup);
if (!empty($search_label)) {
$select->condition('ls.source', "%$search_label%", 'LIKE');
}
if (!empty($missing_target_language) && in_array($missing_target_language, $languages)) {
$select->isNull("lt_$missing_target_language.language");
}
// Join locale targets for each language.
// We want all joined fields to be named as langcodes, but langcodes could
// contain hyphens in their names, which is not allowed by the most database
// engines. So we create a langcode-to-filed_alias map, and rename fields
// later.
$langcode_to_filed_alias_map = array();
foreach ($languages as $langcode) {
$table_alias = $select->leftJoin('locales_target', db_escape_field("lt_$langcode"), "ls.lid = %alias.lid AND %alias.language = '$langcode'");
$langcode_to_filed_alias_map[$langcode] = $select->addField($table_alias, 'language');
}
$select = $select->extend('PagerDefault')->limit(variable_get('tmgmt_source_list_limit', 20));
$rows = $select->execute()->fetchAll();
foreach ($rows as $row) {
foreach ($langcode_to_filed_alias_map as $langcode => $field_alias) {
$row->{$langcode} = $row->{$field_alias};
unset($row->{$field_alias});
}
}
return $rows;
}
/**
* Gets overview form header.
*
* @return array
* Header array definition as expected by theme_tablesort().
*/
public function overviewFormHeader() {
$languages = array();
foreach (language_list() as $langcode => $language) {
$languages['langcode-' . $langcode] = array(
'data' => check_plain($language->name),
);
}
$header = array(
'source' => array('data' => t('Source text')),
) + $languages;
return $header;
}
/**
* Implements TMGMTSourceUIControllerInterface::overviewForm().
*/
public function overviewForm($form, &$form_state, $type) {
$form += $this->overviewSearchFormPart($form, $form_state, $type);
$form['items'] = array(
'#type' => 'tableselect',
'#header' => $this->overviewFormHeader($type),
'#empty' => t('No strings matching given criteria have been found.')
);
$search_data = $this->getSearchFormSubmittedParams();
$strings = $this->getStrings($type, $search_data['label'], $search_data['missing_target_language']);
foreach ($this->getTranslationData($strings, $type) as $id => $data) {
$form['items']['#options'][$id] = $this->overviewRow($type, $data);
}
$form['pager'] = array('#markup' => theme('pager', array('tags' => NULL)));
return $form;
}
/**
* Helper function to create translation data list for the sources page list.
*
* @param array $strings
* Result of the search query returned by tmgmt_i18n_string_get_strings().
* @param string $type
* I18n object type.
*
* @return array
* Structured array with translation data.
*/
protected function getTranslationData($strings, $type) {
$objects = array();
// Source language of locale strings is always english.
$source_language = 'en';
foreach ($strings as $string) {
$id = $string->lid;
// Get existing translations and current job items for the entity
// to determine translation statuses
$current_job_items = tmgmt_job_item_load_latest('locale', $type, $id, $source_language);
$objects[$id] = array(
'id' => $id,
'object' => $string
);
// Load entity translation specific data.
foreach (language_list() as $langcode => $language) {
$translation_status = 'current';
if ($langcode == $source_language) {
$translation_status = 'original';
}
elseif ($string->{$langcode} === NULL) {
$translation_status = 'missing';
}
$objects[$id]['current_job_items'][$langcode] = isset($current_job_items[$langcode]) ? $current_job_items[$langcode] : NULL;
$objects[$id]['translation_statuses'][$langcode] = $translation_status;
}
}
return $objects;
}
/**
* Builds search form for entity sources overview.
*
* @param array $form
* Drupal form array.
* @param $form_state
* Drupal form_state array.
* @param $type
* Entity type.
*
* @return array
* Drupal form array.
*/
public function overviewSearchFormPart($form, &$form_state, $type) {
$options = array();
foreach (language_list() as $langcode => $language) {
$options[$langcode] = $language->name;
}
$default_values = $this->getSearchFormSubmittedParams();
$form['search_wrapper'] = array(
'#prefix' => '<div class="tmgmt-sources-wrapper tmgmt-i18n_string-sources-wrapper">',
'#suffix' => '</div>',
'#weight' => -15,
);
$form['search_wrapper']['search'] = array(
'#tree' => TRUE,
);
$form['search_wrapper']['search']['label'] = array(
'#type' => 'textfield',
'#title' => t('Source text'),
'#default_value' => isset($default_values['label']) ? $default_values['label'] : NULL,
);
// Unset English as it is the source language for all locale strings.
unset($options['en']);
$form['search_wrapper']['search']['missing_target_language'] = array(
'#type' => 'select',
'#title' => t('Not translated to'),
'#options' => $options,
'#empty_option' => '--',
'#default_value' => isset($default_values['missing_target_language']) ? $default_values['missing_target_language'] : NULL,
);
$form['search_wrapper']['search_submit'] = array(
'#type' => 'submit',
'#value' => t('Search'),
);
return $form;
}
/**
* Gets submitted search params.
*
* @return array
*/
public function getSearchFormSubmittedParams() {
$params = array(
'label' => NULL,
'missing_target_language' => NULL,
);
if (isset($_GET['label'])) {
$params['label'] = $_GET['label'];
}
if (isset($_GET['missing_target_language'])) {
$params['missing_target_language'] = $_GET['missing_target_language'];
}
return $params;
}
/**
* Builds a table row for overview form.
*
* @param string $type
* i18n type.
* @param array $data
* Data needed to build the list row.
*
* @return array
*/
public function overviewRow($type, $data) {
// Set the default item key, assume it's the first.
$source = $data['object'];
$row = array(
'id' => $data['id'],
'source' => check_plain($source->source),
);
foreach (language_list() as $langcode => $language) {
$row['langcode-' . $langcode] = theme('tmgmt_ui_translation_language_status_single', array(
'translation_status' => $data['translation_statuses'][$langcode],
'job_item' => isset($data['current_job_items'][$langcode]) ? $data['current_job_items'][$langcode] : NULL,
));
}
return $row;
}
/**
* Implements TMGMTSourceUIControllerInterface::overviewFormSubmit().
*/
public function overviewFormSubmit($form, &$form_state, $type) {
// Handle search redirect.
$this->overviewSearchFormRedirect($form, $form_state, $type);
$items = array_filter($form_state['values']['items']);
$type = $form_state['item_type'];
$source_lang = 'en';
// Create only single job for all items as the source language is just
// the same for all.
$job = tmgmt_job_create($source_lang, NULL, $GLOBALS['user']->uid);
// Loop through entities and create individual jobs for each source language.
foreach ($items as $item) {
$job->addItem('locale', $type, $item);
}
$form_state['redirect'] = array('admin/tmgmt/jobs/' . $job->tjid,
array('query' => array('destination' => current_path())));
drupal_set_message(t('One job needs to be checked out.'));
}
/**
* Performs redirect with search params appended to the uri.
*
* In case of triggering element is edit-search-submit it redirects to
* current location with added query string containing submitted search form
* values.
*
* @param array $form
* Drupal form array.
* @param $form_state
* Drupal form_state array.
* @param $type
* Entity type.
*/
public function overviewSearchFormRedirect($form, &$form_state, $type) {
if ($form_state['triggering_element']['#id'] == 'edit-search-submit') {
$query = array();
foreach ($form_state['values']['search'] as $key => $value) {
$query[$key] = $value;
}
drupal_goto($_GET['q'], array('query' => $query));
}
}
}

View File

@@ -0,0 +1,105 @@
<?php
/**
* Basic Locale Source tests.
*/
class TMGMTLocaleSourceUiTestCase extends TMGMTBaseTestCase {
static function getInfo() {
return array(
'name' => 'Locale Source UI tests',
'description' => 'Tests the locale source overview',
'group' => 'Translation Management',
);
}
function setUp() {
parent::setUp(array('tmgmt_locale', 'tmgmt_ui'));
$this->langcode = 'de';
$this->context = 'default';
$file = new stdClass();
$file->uri = drupal_realpath(drupal_get_path('module', 'tmgmt_locale') . '/tests/test.xx.po');
$this->pofile = file_save($file);
$this->setEnvironment($this->langcode);
$this->setEnvironment('gsw-berne');
}
public function testOverview() {
// Load PO file to create a locale structure in the database.
_locale_import_po($this->pofile, $this->langcode, LOCALE_IMPORT_OVERWRITE, $this->context);
$this->loginAsTranslator();
$this->drupalGet('admin/tmgmt/sources/locale_default');
$this->assertText('Hello World');
$this->assertText('Example');
$rows = $this->xpath('//tbody/tr');
foreach ($rows as $row) {
if ($row->td[1] == 'Hello World') {
$this->assertEqual((string) $row->td[3]->div['title'], t('Translation up to date'));
$this->assertEqual((string) $row->td[4]->div['title'], t('Not translated'));
}
}
// Filter on the label.
$edit = array('search[label]' => 'Hello');
$this->drupalPost(NULL, $edit, t('Search'));
$this->assertText('Hello World');
$this->assertNoText('Example');
$locale_object = db_query('SELECT * FROM {locales_source} WHERE source = :source LIMIT 1', array(':source' => 'Hello World'))->fetchObject();
// First add source to the cart to test its functionality.
$edit = array(
'items[' . $locale_object->lid . ']' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Add to cart'));
$this->assertRaw(t('@count content source was added into the <a href="@url">cart</a>.', array('@count' => 1, '@url' => url('admin/tmgmt/cart'))));
$edit['target_language[]'] = array('gsw-berne');
$this->drupalPost('admin/tmgmt/cart', $edit, t('Request translation'));
// Assert that the job item is displayed.
$this->assertText('Hello World');
$this->assertText(t('Locale'));
$this->assertText('2');
$this->drupalPost(NULL, array('target_language' => 'gsw-berne'), t('Submit to translator'));
// Test for the translation flag title.
$this->drupalGet('admin/tmgmt/sources/locale_default');
$this->assertRaw(t('Active job item: Needs review'));
// Review and accept the job item.
$job_items = tmgmt_job_item_load_latest('locale', 'default', $locale_object->lid, 'en');
$this->drupalGet('admin/tmgmt/items/' . $job_items['gsw-berne']->tjiid);
$this->assertRaw('gsw-berne_Hello World');
$this->drupalPost(NULL, array(), t('Save as completed'));
$this->drupalGet('admin/tmgmt/sources/locale_default');
$this->assertNoRaw(t('Active job item: Needs review'));
$rows = $this->xpath('//tbody/tr');
foreach ($rows as $row) {
if ($row->td[1] == 'Hello World') {
$this->assertEqual((string) $row->td[3]->div['title'], t('Translation up to date'));
$this->assertEqual((string) $row->td[4]->div['title'], t('Translation up to date'));
}
}
// Test the missing translation filter.
$this->drupalGet('admin/tmgmt/sources/locale_default');
// Check that the source language (en) has been removed from the target language
// select box.
$elements = $this->xpath('//select[@name=:name]//option[@value=:option]', array(':name' => 'search[target_language]', ':option' => 'en'));
$this->assertTrue(empty($elements));
// Filter on the "Not translated to".
$edit = array('search[missing_target_language]' => 'gsw-berne');
$this->drupalPost(NULL, $edit, t('Search'));
// Hello World is translated to "gsw-berne" therefore it must not show up in the
// list.
$this->assertNoText('Hello World');
}
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* @file
* Hooks provided by the node source plugin for TMGMT.
*/
/**
* Alter the created node translation.
*
* @param object
* $tnode translated node
* @param object
* $node source node
* @param TMGMTJobItem
* $job_item
*/
function hook_tmgmt_before_update_node_translation_alter($tnode, $node, $job_item) {
// Always store new translations as a new revision.
$tnode->revision = 1;
}

View File

@@ -0,0 +1,26 @@
name = Content Source
description = Content Translation source plugin for the Translation Management system.
package = Translation Management
core = 7.x
dependencies[] = tmgmt
dependencies[] = tmgmt_field
dependencies[] = translation
files[] = tmgmt_node.plugin.inc
files[] = tmgmt_node.ui.inc
files[] = tmgmt_node.test
; Views integration and handlers
files[] = views/tmgmt_node.views.inc
files[] = views/handlers/tmgmt_node_handler_field_translation_language_status.inc
files[] = views/handlers/tmgmt_node_handler_field_translation_language_status_single.inc
files[] = views/handlers/tmgmt_node_handler_filter_node_translatable_types.inc
files[] = views/handlers/tmgmt_node_handler_filter_missing_translation.inc
; Information added by Drupal.org packaging script on 2016-09-21
version = "7.x-1.0-rc2+1-dev"
core = "7.x"
project = "tmgmt"
datestamp = "1474446494"

View File

@@ -0,0 +1,35 @@
<?php
/**
* @file
* Source plugin for the Translation Management system that handles nodes.
*/
/**
* Implements hook_tmgmt_source_plugin_info().
*/
function tmgmt_node_tmgmt_source_plugin_info() {
$info['node'] = array(
'label' => t('Node'),
'description' => t('Source handler for nodes.'),
'plugin controller class' => 'TMGMTNodeSourcePluginController',
'ui controller class' => 'TMGMTNodeSourceUIController',
'views controller class' => 'TMGMTNodeSourceViewsController',
'item types' => array(),
);
foreach (node_type_get_names() as $type => $name) {
if (translation_supported_type($type)) {
$info['node']['item types'][$type] = $name;
}
}
return $info;
}
/**
* Form element validator for the missing target language views field handler.
*/
function tmgmt_node_views_exposed_target_language_validate($form, &$form_state) {
if (!empty($form_state['values']['tmgmt_node_missing_translation']) && $form_state['values']['language_1'] == $form_state['values']['tmgmt_node_missing_translation']) {
form_set_error('tmgmt_node_missing_translation', t('The source and target languages must not be the same.'));
}
}

View File

@@ -0,0 +1,152 @@
<?php
/**
* @file
* Provides the node source plugin controller.
*/
class TMGMTNodeSourcePluginController extends TMGMTDefaultSourcePluginController {
/**
* {@inheritdoc}
*
* Returns the data from the fields as a structure that can be processed by
* the Translation Management system.
*/
public function getData(TMGMTJobItem $job_item) {
$node = node_load($job_item->item_id);
$source_language = $job_item->getJob()->source_language;
$languages = language_list();
// If the node language is not the same as the job source language try to
// load its translation for the job source language.
if ($node->language != $source_language) {
$translation_loaded = FALSE;
foreach (translation_node_get_translations($node->nid) as $language => $translation) {
if ($language == $source_language) {
$node = node_load($translation->nid);
$translation_loaded = TRUE;
break;
}
}
if (!$translation_loaded) {
throw new TMGMTException(t('Unable to load %language translation for the node %title',
array('%language' => $languages[$source_language]->name, '%title' => $node->title)));
}
}
$type = node_type_get_type($node);
// Get all the fields that can be translated and arrange their values into
// a specific structure.
$structure = tmgmt_field_get_source_data('node', $node, $job_item->getJob()->source_language);
$structure['node_title']['#label'] = $type->title_label;
$structure['node_title']['#text'] = $node->title;
return $structure;
}
/**
* {@inheritdoc}
*/
public function saveTranslation(TMGMTJobItem $job_item) {
if ($node = node_load($job_item->item_id)) {
$job = $job_item->getJob();
if (empty($node->tnid)) {
// We have no translation source nid, this is a new set, so create it.
$node->tnid = $node->nid;
node_save($node);
}
$translations = translation_node_get_translations($node->tnid);
if (isset($translations[$job->target_language])) {
// We have already a translation for the source node for the target
// language, so load it.
$tnode = node_load($translations[$job->target_language]->nid);
}
else {
// We don't have a translation for the source node yet, so create one.
$tnode = clone $node;
unset($tnode->nid, $tnode->vid, $tnode->uuid, $tnode->vuuid);
$tnode->language = $job->target_language;
$tnode->translation_source = $node;
}
// Allow modules and translator plugins to alter, for example in the
// case of creating revisions for translated nodes, or altering
// properties of the tnode before saving.
drupal_alter('tmgmt_before_update_node_translation', $tnode, $node, $job_item);
// Time to put the translated data into the node.
$data = $job_item->getData();
// Special case for the node title.
if (isset($data['node_title']['#translation']['#text'])) {
$tnode->title = $data['node_title']['#translation']['#text'];
unset($data['node_title']);
}
tmgmt_field_populate_entity('node', $tnode, $job->target_language, $data, FALSE);
// Reset translation field, which determines outdated status.
$tnode->translation['status'] = 0;
node_save($tnode);
// We just saved the translation, set the sate of the job item to
// 'finished'.
$job_item->accepted();
}
}
/**
* {@inheritdoc}
*/
public function getLabel(TMGMTJobItem $job_item) {
if ($node = node_load($job_item->item_id)) {
return entity_label('node', $node);
}
return parent::getLabel($job_item);
}
/**
* {@inheritdoc}
*/
public function getUri(TMGMTJobItem $job_item) {
if ($node = node_load($job_item->item_id)) {
return entity_uri('node', $node);
}
return parent::getUri($job_item);
}
/**
* {@inheritdoc}
*/
public function getType(TMGMTJobItem $job_item) {
if ($node = node_load($job_item->item_id)) {
return node_type_get_name($node);
}
return parent::getType($job_item);
}
/**
* {@inheritdoc}
*/
public function getSourceLangCode(TMGMTJobItem $job_item) {
if ($node = node_load($job_item->item_id)) {
return entity_language('node', $node);
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function getExistingLangCodes(TMGMTJobItem $job_item) {
$existing_lang_codes = array();
if ($node = node_load($job_item->item_id)) {
$existing_lang_codes = array(entity_language('node', $node));
}
if ($translations = translation_node_get_translations($job_item->item_id)) {
$existing_lang_codes = array_unique(array_merge($existing_lang_codes, array_keys($translations)));
}
return $existing_lang_codes;
}
}

View File

@@ -0,0 +1,147 @@
<?php
/**
* Basic Node Source tests.
*/
class TMGMTNodeSourceTestCase extends TMGMTEntityTestCaseUtility {
static function getInfo() {
return array(
'name' => 'Node Source tests',
'description' => 'Exporting source data from nodes and saving translations back to nodes',
'group' => 'Translation Management',
);
}
function setUp() {
parent::setUp(array('tmgmt_node', 'translation'));
$this->loginAsAdmin();
$this->setEnvironment('de');
$this->createNodeType('page', 'Basic page', TRANSLATION_ENABLED, FALSE);
$this->attachFields('node', 'page', array(TRUE, TRUE, FALSE, FALSE));
}
/**
* Tests nodes field translation.
*/
function testNodeSource() {
// Create a translation job.
$job = $this->createJob();
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
for ($i = 0; $i < 2; $i++) {
$node = $this->createNode('page');
// Create a job item for this node and add it to the job.
$item = $job->addItem('node', 'node', $node->nid);
$this->assertEqual('Basic page', $item->getSourceType());
}
// Translate the job.
$job->requestTranslation();
foreach ($job->getItems() as $item) {
// The source is only available in en.
$this->assertJobItemLangCodes($item, 'en', array('en'));
$item->acceptTranslation();
$node = node_load($item->item_id);
// Check if the tnid attribute is bigger than 0.
$this->assertTrue($node->tnid > 0, 'The source node is part of a translation set.');
// The translations may be statically cached, so make make sure
// to reset the cache before loading the node translations.
$cached_translations = & drupal_static('translation_node_get_translations', array());
unset($cached_translations[$node->tnid]);
// Load the translation set of the source node.
$translations = translation_node_get_translations($node->tnid);
$this->assertNotNull($translations['de'], 'Translation found for the source node.');
if (isset($translations['de'])) {
$tnode = node_load($translations['de']->nid, NULL, TRUE);
$this->checkTranslatedData($tnode, $item->getData(), 'de');
}
// The source should be now available for en and de.
$this->assertJobItemLangCodes($item, 'en', array('de', 'en'));
}
}
/**
* Test if the source is able to pull content in requested language.
*/
function testRequestDataForSpecificLanguage() {
$this->setEnvironment('sk');
$this->setEnvironment('es');
$content_type = $this->drupalCreateContentType();
$node = $this->drupalCreateNode(array(
'title' => $this->randomName(),
'language' => 'sk',
'body' => array('sk' => array(array())),
'type' => $content_type->type,
));
$this->drupalCreateNode(array(
'title' => 'en translation',
'language' => 'en',
'tnid' => $node->nid,
'body' => array('en' => array(array())),
'type' => $content_type->type,
));
// Create a translation job.
$job = $this->createJob('en', 'de');
$job->save();
$job->addItem('node', 'node', $node->nid);
$data = $job->getData();
$this->assertEqual($data[1]['node_title']['#text'], 'en translation');
// Create new job item with a source language for which the translation
// does not exit.
$job = $this->createJob('es', 'cs');
$job->save();
try {
$job->addItem('node', 'node', $node->nid);
$this->fail('The job item should not be added as there is no translation for language "es"');
}
catch (TMGMTException $e) {
$languages = language_list();
$this->assertEqual(t('Unable to load %language translation for the node %title',
array('%language' => $languages['es']->name, '%title' => $node->title)), $e->getMessage());
}
}
/**
* Compares the data from an entity with the translated data.
*
* @param $node
* The translated node object.
* @param $data
* An array with the translated data.
* @param $langcode
* The code of the target language.
*/
function checkTranslatedData($node, $data, $langcode) {
foreach (element_children($data) as $field_name) {
if ($field_name == 'node_title') {
$this->assertEqual($node->title, $data['node_title']['#translation']['#text'], 'The title of the translated node matches the translated data.');
continue;
}
foreach (element_children($data[$field_name]) as $delta) {
$field_langcode = field_is_translatable('node', field_info_field($field_name)) ? $langcode : LANGUAGE_NONE;
foreach (element_children($data[$field_name][$delta]) as $column) {
$column_value = $data[$field_name][$delta][$column];
if (!isset($column_value['#translate']) || $column_value['#translate']) {
$this->assertEqual($node->{$field_name}[$field_langcode][$delta][$column], $column_value['#translation']['#text'], format_string('The translatable field %field:%delta has been populated with the proper translated data.', array(
'%field' => $field_name,
'delta' => $delta
)));
}
}
}
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* @file
* Provides the node source ui controller.
*/
class TMGMTNodeSourceUIController extends TMGMTDefaultSourceUIController {
/**
* {@inheritdoc}
*/
public function hook_menu() {
// The node source overview is a View using Views Bulk Operations. Therefore
// we don't need to provide any menu items.
return array();
}
/**
* {@inheritdoc}
*/
public function hook_forms() {
// The node source overview is a View using Views Bulk Operations. Therefore
// we don't need to provide any forms.
return array();
}
/**
* {@inheritdoc}
*/
public function hook_views_default_views() {
return _tmgmt_load_exports('tmgmt_node', 'views', 'view.inc', 'view');
}
}

View File

@@ -0,0 +1,22 @@
name = Content Source User Interface
description = User Interface for the content translation source plugin.
package = Translation Management
core = 7.x
dependencies[] = tmgmt_node
dependencies[] = tmgmt_ui
dependencies[] = views_bulk_operations
files[] = tmgmt_node_ui.test
files[] = tmgmt_node_ui.overview.test
; Views handlers
files[] = views/tmgmt_node_ui_handler_filter_node_translatable_types.inc
files[] = views/tmgmt_node_ui_handler_field_jobs.inc
; Information added by Drupal.org packaging script on 2016-09-21
version = "7.x-1.0-rc2+1-dev"
core = "7.x"
project = "tmgmt"
datestamp = "1474446494"

View File

@@ -0,0 +1,93 @@
<?php
/**
* @file
* Main module file for the translation management node source plugin user
* interface.
*/
/**
* Implements hook_page_alter().
*/
function tmgmt_node_ui_page_alter(&$page) {
// Translation tabs for nodes.
if (($node = menu_get_object()) && entity_access('create', 'tmgmt_job')) {
if (isset($page['content']['system_main']['translation_node_overview'])) {
module_load_include('inc', 'tmgmt_node_ui', 'tmgmt_node_ui.pages');
$page['content']['system_main']['translation_node_overview'] = drupal_get_form('tmgmt_node_ui_node_form', $node, $page['content']['system_main']['translation_node_overview']);
}
// Support the context module: when context is used for placing the
// system_main block, then block contents appear nested one level deeper
// under another 'content' key.
elseif (isset($page['content']['system_main']['content']['translation_node_overview'])) {
module_load_include('inc', 'tmgmt_node_ui', 'tmgmt_node_ui.pages');
$page['content']['system_main']['content']['translation_node_overview'] = drupal_get_form('tmgmt_node_ui_node_form', $node, $page['content']['system_main']['content']['translation_node_overview']);
}
}
}
/**
* Implements hook_action_info().
*/
function tmgmt_node_ui_action_info() {
return array(
'tmgmt_node_ui_checkout_multiple_action' => array(
'type' => 'node',
'label' => t('Request translations'),
'configurable' => false,
'aggregate' => true
)
);
}
/**
* Action to do multistep checkout for translations.
*
* @param array $nodes
* Array of Drupal nodes.
* @param $info
* Action info - not used.
*
*/
function tmgmt_node_ui_checkout_multiple_action($nodes, $info) {
$jobs = array();
$source_lang_registry = array();
// Loop through entities and create individual jobs for each source language.
foreach ($nodes as $node) {
try {
// For given source lang no job exists yet.
if (!isset($source_lang_registry[$node->language])) {
// Create new job.
$job = tmgmt_job_create($node->language, NULL, $GLOBALS['user']->uid);
// Add initial job item.
$job->addItem('node', 'node', $node->nid);
// Add job identifier into registry
$source_lang_registry[$node->language] = $job->tjid;
// Add newly created job into jobs queue.
$jobs[$job->tjid] = $job;
}
// We have a job for given source lang, so just add new job item for the
// existing job.
else {
$jobs[$source_lang_registry[$node->language]]->addItem('node', 'node', $node->nid);
}
}
catch (TMGMTException $e) {
watchdog_exception('tmgmt', $e);
drupal_set_message(t('Unable to add job item for node %name. Make sure the source content is not empty.', array('%name' => $node->title)), 'error');
}
}
// If necessary, do a redirect.
$redirects = tmgmt_ui_job_checkout_multiple($jobs);
if ($redirects) {
tmgmt_ui_redirect_queue_set($redirects, current_path());
drupal_set_message(format_plural(count($redirects), t('One job needs to be checked out.'), t('@count jobs need to be checked out.')));
drupal_goto(tmgmt_ui_redirect_queue_dequeue());
}
}

View File

@@ -0,0 +1,160 @@
<?php
/**
* Content Overview Tests
*/
class TMGMTNodeSourceUIOverviewTestCase extends TMGMTEntityTestCaseUtility {
static function getInfo() {
return array(
'name' => 'Node Source UI Overview tests',
'description' => 'Tests the user interface for node overviews.',
'group' => 'Translation Management',
'dependencies' => array('rules'),
);
}
function setUp() {
parent::setUp(array('tmgmt_node_ui'));
$this->loginAsAdmin();
$this->setEnvironment('de');
$this->setEnvironment('fr');
$this->setEnvironment('es');
$this->setEnvironment('el');
$this->createNodeType('page', 'Page', TRANSLATION_ENABLED, FALSE);
// 1 means that the node type can have a language but is not translatable.
$this->createNodeType('untranslated', 'Untranslated', 1, FALSE);
$this->checkPermissions(array(), TRUE);
// Allow auto-accept.
$default_translator = tmgmt_translator_load('test_translator');
$default_translator->settings = array(
'auto_accept' => TRUE,
);
$default_translator->save();
}
/**
* Tests translating through the content source overview.
*/
function testNodeSourceOverview() {
// Login as translator to translate nodes.
$this->loginAsTranslator(array(
'translate content',
'edit any page content',
'create page content',
));
// Create a bunch of english nodes.
$node1 = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
$node2 = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
$node3 = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
$node4 = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
// Create a node with an undefined language.
$node5 = $this->drupalCreateNode(array('type' => 'page'));
// Create a node of an untranslatable content type.
$node6 = $this->drupalCreateNode(array('type' => 'untranslated', 'language' => 'en', 'body' => array('en' => array(array()))));
// Go to the overview page and make sure the nodes are there.
$this->drupalGet('admin/tmgmt/sources/node');
// Make sure that valid nodes are shown.
$this->assertText($node1->title);
$this->assertText($node2->title);
$this->assertText($node3->title);
$this->assertText($node4->title);
// Nodes without a language must not be shown.
$this->assertNoText($node5->title);
// Node with a type that is not enabled for translation must not be shown.
$this->assertNoText($node6->title);
// Now translate them.
$edit = array(
'views_bulk_operations[0]' => TRUE,
'views_bulk_operations[1]' => TRUE,
'views_bulk_operations[2]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translations'));
// Some assertions on the submit form.
$this->assertText(t('@title and 2 more (English to ?, Unprocessed)', array('@title' => $node1->title)));
$this->assertText($node1->title);
$this->assertText($node2->title);
$this->assertText($node3->title);
$this->assertNoText($node4->title);
// Translate
$edit = array(
'target_language' => 'de',
);
$this->drupalPost(NULL, $edit, t('Submit to translator'));
$this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node1->title, '@language' => t('German'))));
$this->assertText(t('The translation for @title has been accepted.', array('@title' => $node1->title)));
$this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node2->title, '@language' => t('German'))));
$this->assertText(t('The translation for @title has been accepted.', array('@title' => $node1->title)));
$this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node3->title, '@language' => t('German'))));
$this->assertText(t('The translation for @title has been accepted.', array('@title' => $node1->title)));
// Check the translated node.
$this->clickLink($node1->title);
$this->clickLink(t('Translate'));
$this->assertText('de_' . $node1->title);
// Test for the source list limit set in the views export.
$view = views_get_view('tmgmt_node_source_overview');
$view->execute_display('default');
$this->assertEqual($view->get_items_per_page(), variable_get('tmgmt_source_list_limit', 20));
// Test the missing translation filter.
// Create nodes needed to test the missing translation filter here so that
// VBO order is not affected.
$node_not_translated = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
$node_de = $this->drupalCreateNode(array('type' => 'page', 'language' => 'de', 'body' => array('de' => array(array()))));
$this->drupalGet('admin/tmgmt/sources/node');
$this->assertText($node1->title);
$this->assertText($node_not_translated->title);
$this->assertText($node_de->title);
// Submitting the search form will not work. After the form submission the
// page does gets redirected to url without query parameters. So we simply
// access the page with desired query.
$this->drupalGet('admin/tmgmt/sources/node', array('query' => array(
'tmgmt_node_missing_translation' => 'de',
'target_status' => 'untranslated',
)));
$this->assertNoText($node1->title);
$this->assertText($node_not_translated->title);
$this->assertNoText($node_de->title);
// Update the the translate flag of the translated node and test if it is
// listed among sources with missing translation.
db_update('node')->fields(array('translate' => 1))
->condition('nid', $node1->nid)->execute();
$this->drupalGet('admin/tmgmt/sources/node', array('query' => array(
'tmgmt_node_missing_translation' => 'de',
'target_status' => 'outdated',
)));
$this->assertText($node1->title);
$this->assertNoText($node_not_translated->title);
$this->assertNoText($node_de->title);
$this->drupalGet('admin/tmgmt/sources/node', array('query' => array(
'tmgmt_node_missing_translation' => 'de',
'target_status' => 'untranslated_or_outdated',
)));
$this->assertText($node1->title);
$this->assertText($node_not_translated->title);
$this->assertNoText($node_de->title);
}
}

View File

@@ -0,0 +1,116 @@
<?php
/**
* @file
* Provides page and form callbacks for the Translation Management Tool Node
* Source User Interface module.
*/
/**
* Node translation overview form. This form overrides the Drupal core or i18n
* Content Translation page with a tableselect form.
*/
function tmgmt_node_ui_node_form($form, &$form_state, $node, $original) {
// Store the node in the form state so we can easily create the job in the
// submit handler.
$form_state['node'] = $node;
$form['top_actions']['#type'] = 'actions';
$form['top_actions']['#weight'] = -10;
tmgmt_ui_add_cart_form($form['top_actions'], $form_state, 'node', 'node', $node->nid);
// Inject our additional column into the header.
array_splice($original['#header'], -1, 0, array(t('Pending Translations')));
// Make this a tableselect form.
$form['languages'] = array(
'#type' => 'tableselect',
'#header' => $original['#header'],
'#options' => array(),
);
$languages = module_exists('i18n_node') ? i18n_node_language_list($node) : language_list();
// Check if there is a job / job item that references this translation.
$items = tmgmt_job_item_load_latest('node', 'node', $node->nid, $node->language);
foreach ($languages as $langcode => $language) {
if ($langcode == LANGUAGE_NONE) {
// Never show language neutral on the overview.
continue;
}
// Since the keys are numeric and in the same order we can shift one element
// after the other from the original non-form rows.
$option = array_shift($original['#rows']);
if ($langcode == $node->language) {
$additional = '<strong>' . t('Source') . '</strong>';
// This is the source object so we disable the checkbox for this row.
$form['languages'][$langcode] = array(
'#type' => 'checkbox',
'#disabled' => TRUE,
);
}
elseif (isset($items[$langcode])) {
/** @var TMGMTJobItem $item */
$item = $items[$langcode];
if ($item->getJob()->isUnprocessed()) {
$uri = $item->getJob()->uri();
$additional = l(t('Unprocessed'), $uri['path']);
}
else {
$wrapper = entity_metadata_wrapper('tmgmt_job_item', $item);
$uri = $item->uri();
$additional = l($wrapper->state->label(), $uri['path']);
}
// Disable the checkbox for this row since there is already a translation
// in progress that has not yet been finished. This way we make sure that
// we don't stack multiple active translations for the same item on top
// of each other.
$form['languages'][$langcode] = array(
'#type' => 'checkbox',
'#disabled' => TRUE,
);
}
else {
// There is no translation job / job item for this target language.
$additional = t('None');
}
// Inject the additional column into the array.
array_splice($option, -1, 0, array($additional));
// Append the current option array to the form.
$form['languages']['#options'][$langcode] = $option;
}
$form['actions']['#type'] = 'actions';
$form['actions']['request'] = array(
'#type' => 'submit',
'#value' => t('Request translation'),
'#submit' => array('tmgmt_node_ui_translate_form_submit'),
'#validate' => array('tmgmt_node_ui_translate_form_validate'),
);
return $form;
}
/**
* Validation callback for the node translation overview form.
*/
function tmgmt_node_ui_translate_form_validate($form, &$form_state) {
$selected = array_filter($form_state['values']['languages']);
if (empty($selected)) {
form_set_error('languages', t('You have to select at least one language for requesting a translation.'));
}
}
/**
* Submit callback for the node translation overview form.
*/
function tmgmt_node_ui_translate_form_submit($form, &$form_state) {
$node = $form_state['node'];
$values = $form_state['values'];
$jobs = array();
foreach (array_keys(array_filter($values['languages'])) as $langcode) {
// Create the job object.
$job = tmgmt_job_create($node->language, $langcode, $GLOBALS['user']->uid);
// Add the job item.
$job->addItem('node', 'node', $node->nid);
// Append this job to the array of created jobs so we can redirect the user
// to a multistep checkout form if necessary.
$jobs[$job->tjid] = $job;
}
tmgmt_ui_job_checkout_and_redirect($form_state, $jobs);
}

View File

@@ -0,0 +1,49 @@
<?php
/*
* @file
* Contains default rules.
*/
/**
* Implements hook_default_rules_configuration().
*/
function tmgmt_node_ui_default_rules_configuration() {
$data = '{ "tmgmt_node_ui_request_translation" : {
"LABEL" : "Request translation",
"PLUGIN" : "rule",
"REQUIRES" : [ "tmgmt" ],
"USES VARIABLES" : { "nodes" : { "label" : "Nodes", "type" : "list\u003Cnode\u003E" } },
"DO" : [
{ "tmgmt_get_first_from_node_list" : {
"USING" : { "list" : [ "nodes" ] },
"PROVIDE" : { "first_node" : { "first_node" : "Node" } }
}
},
{ "tmgmt_rules_create_job" : {
"USING" : { "source_language" : [ "first-node:language" ] },
"PROVIDE" : { "job" : { "job" : "Job" } }
}
},
{ "LOOP" : {
"USING" : { "list" : [ "nodes" ] },
"ITEM" : { "node" : "Node" },
"DO" : [
{ "tmgmt_rules_job_add_item" : {
"job" : [ "job" ],
"plugin" : "node",
"item_type" : "node",
"item_id" : [ "node:nid" ]
}
}
]
}
},
{ "tmgmt_rules_job_checkout" : { "job" : [ "job" ] } }
]
}
}';
$rule = rules_import($data);
$configs[$rule->name] = $rule;
return $configs;
}

View File

@@ -0,0 +1,15 @@
#edit-tmgmt-node-missing-translation-wrapper .form-item-target-status,
#edit-tmgmt-node-missing-translation-wrapper .form-item-tmgmt-node-missing-translation {
float: left;
}
#edit-tmgmt-node-missing-translation-wrapper .form-item-target-status {
margin: 0 0 0 55px;
padding: 0;
position: relative;
top: -19px;
}
#edit-tmgmt-node-missing-translation-wrapper .form-item-target-status select {
margin-top: 9px;
}

View File

@@ -0,0 +1,450 @@
<?php
/**
* Basic Node Source UI tests.
*/
class TMGMTNodeSourceUITestCase extends TMGMTEntityTestCaseUtility {
static function getInfo() {
return array(
'name' => 'Node Source UI tests',
'description' => 'Tests the user interface for node translation sources.',
'group' => 'Translation Management',
);
}
function setUp() {
parent::setUp(array('tmgmt_node_ui', 'block'));
// We need the administer blocks permission.
$this->loginAsAdmin(array('administer blocks'));
$this->setEnvironment('de');
$this->setEnvironment('fr');
$this->setEnvironment('es');
$this->setEnvironment('el');
// @todo Re-enable this when switching to testing profile.
// Enable the main page content block for hook_page_alter() to work.
$edit = array(
'blocks[system_main][region]' => 'content',
);
$this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
$this->createNodeType('page', 'Page', TRANSLATION_ENABLED, FALSE);
}
/**
* Tests the create, submit and accept permissions.
*/
function testPermissions() {
$no_permissions = $this->drupalCreateUser();
$this->drupalLogin($no_permissions);
$this->drupalGet('admin/tmgmt');
$this->assertResponse(403);
// Test with a user that is only allowed to create jobs.
$create_user = $this->drupalCreateUser(array('access administration pages', 'translate content', 'create translation jobs'));
$this->drupalLogin($create_user);
// Create an english source node.
$node = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
// Go to the translate tab.
$this->drupalGet('node/' . $node->nid);
$this->clickLink('Translate');
// Request a translation for german.
$edit = array(
'languages[de]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
$this->assertText(t('One job has been created.'));
// Verify that we are still on the translate tab.
$this->assertText(t('Translations of @title', array('@title' => $node->title)));
// The job is unprocessed, check the status flag in the source list.
$this->drupalGet('admin/tmgmt/sources');
$links = $this->xpath('//a[contains(@title, :title)]', array(':title' => t('Active job item: @state', array('@state' => t('Unprocessed')))));
$attributes = $links[0]->attributes();
// Check if the found link points to the job checkout page instead of the
// job item review form.
$this->assertEqual($attributes['href'], url('admin/tmgmt/jobs/1', array('query' => array('destination' => 'admin/tmgmt/sources'))));
$this->drupalGet('admin/tmgmt');
$this->assertResponse(200);
$this->assertLink(t('manage'));
$this->assertNoLink(t('submit'));
$this->assertNoLink(t('delete'));
$this->assertText(t('@title', array('@title' => $node->title)));
$this->clickLink(t('manage'));
$this->assertResponse(200);
$this->assertNoRaw(t('Submit to translator'));
// Try to access the delete page directly.
$this->drupalGet($this->getUrl() . '/delete');
$this->assertResponse(403);
// Log in as user with only submit permission.
$submit_user = $this->drupalCreateUser(array('access administration pages', 'translate content', 'submit translation jobs'));
$this->drupalLogin($submit_user);
// Go to the translate tab, verify that there is no request translation
// button.
$this->drupalGet('node/' . $node->nid);
$this->clickLink('Translate');
$this->assertNoRaw(t('Request translation'));
// Go to the overview and submit the job.
$this->drupalGet('admin/tmgmt');
$this->assertResponse(200);
$this->assertLink(t('submit'));
$this->assertNoLink(t('manage'));
$this->assertNoLink(t('delete'));
$this->assertText(t('@title', array('@title' => $node->title)));
// Check VBO actions - "submit translation job" has the right to cancel
// translation only.
$element = $this->xpath('//select[@id=:id]/option/@value', array(':id' => 'edit-operation'));
$options = array();
foreach ($element as $option) {
$options[] = (string) $option;
}
$this->assertTrue(in_array('rules_component::rules_tmgmt_job_abort_translation', $options));
// Go to the job checkout page and submit it.
$this->clickLink('submit');
$this->drupalPost(NULL, array(), t('Submit to translator'));
// After submit the redirect goes back to the job overview.
$this->assertUrl('admin/tmgmt');
// Make sure that the job is active now.
$this->assertText(t('Active'));
// Click abort link and check if we are at the job abort confirm page.
$this->clickLink(t('abort'));
$this->assertText(t('This will send a request to the translator to abort the job. After the action the job translation process will be aborted and only remaining action will be resubmitting it.'));
// Return back to job overview and test the manage link.
$this->drupalGet('admin/tmgmt');
$this->clickLink(t('manage'));
$this->assertText(t('Needs review'));
$this->assertNoLink(t('review'));
// Now log in as user with only accept permission and review the job.
$accept_user = $this->drupalCreateUser(array('access administration pages', 'accept translation jobs'));
$this->drupalLogin($accept_user);
$this->drupalGet('admin/tmgmt');
// Check VBO actions - "accept translation jobs" has the right to accept
// translation only.
$element = $this->xpath('//select[@id=:id]/option/@value', array(':id' => 'edit-operation'));
$options = array();
foreach ($element as $option) {
$options[] = (string) $option;
}
$this->assertTrue(in_array('rules_component::rules_tmgmt_job_accept_translation', $options));
$this->clickLink('manage');
$this->clickLink('review');
$this->drupalPost(NULL, array(), '✓');
// Verify that the accepted character is shown.
$this->assertText('&#x2611;');
$this->drupalPost(NULL, array(), t('Save as completed'));
$this->assertText(t('Accepted'));
$this->assertText('1/0/0');
$create_user = $this->loginAsAdmin();
$this->drupalLogin($create_user);
$this->drupalGet('admin/tmgmt');
// Check VBO actions - "administer tmgmt" has rights for all actions.
$element = $this->xpath('//select[@id=:id]/option/@value', array(':id' => 'edit-operation'));
$options = array();
foreach ($element as $option) {
$options[] = (string) $option;
}
$this->assertTrue(in_array('rules_component::rules_tmgmt_job_accept_translation', $options));
$this->assertTrue(in_array('rules_component::rules_tmgmt_job_abort_translation', $options));
$this->assertTrue(in_array('rules_component::rules_tmgmt_job_delete', $options));
// Go to the translate tab, verify that there is no request translation
// button.
//$this->drupalGet('node/' . $node->nid);
//$this->clickLink('Translate');
//$this->assertNoRaw(t('Request translation'));
}
/**
* Test the translate tab for a single checkout.
*/
function testTranslateTabSingleCheckout() {
// Create a user that is allowed to translate nodes.
$translater = $this->drupalCreateUser(array('translate content', 'create translation jobs', 'submit translation jobs', 'accept translation jobs'));
$this->drupalLogin($translater);
// Create an english source node.
$node = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
// Go to the translate tab.
$this->drupalGet('node/' . $node->nid);
$this->clickLink('Translate');
// Assert some basic strings on that page.
$this->assertText(t('Translations of @title', array('@title' => $node->title)));
$this->assertText(t('Pending Translations'));
// Request a translation for german.
$edit = array(
'languages[de]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
// Verify that we are on the translate tab.
$this->assertText(t('One job needs to be checked out.'));
$this->assertText($node->title);
// Go to the translate tab and check if the pending translation label is
// "Unprocessed" and links to the job checkout page.
$this->drupalGet('node/' . $node->nid . '/translate');
$this->assertLink(t('Unprocessed'));
$this->clickLink(t('Unprocessed'));
// Submit.
$this->drupalPost(NULL, array(), t('Submit to translator'));
// Make sure that we're back on the translate tab.
$this->assertEqual(url('node/' . $node->nid . '/translate', array('absolute' => TRUE)), $this->getUrl());
$this->assertText(t('Test translation created.'));
$this->assertText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node->title, '@language' => t('German'))));
// Review.
$this->clickLink(t('Needs review'));
// @todo Review job throuh the UI.
$items = tmgmt_job_item_load_latest('node', 'node', $node->nid, 'en');
$items['de']->acceptTranslation();
// German node should now be listed and be clickable.
$this->drupalGet('node/' . $node->nid . '/translate');
$this->clickLink('de_' . $node->title);
// Test that the destination query argument does not break the redirect
// and we are redirected back to the correct page.
$this->drupalGet('node/' . $node->nid . '/translate', array('query' => array('destination' => 'node')));
// Request a spanish translation.
$edit = array(
'languages[es]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
// Verify that we are on the checkout page.
$this->assertText(t('One job needs to be checked out.'));
$this->assertText($node->title);
$this->drupalPost(NULL, array(), t('Submit to translator'));
// Make sure that we're back on the originally defined destination URL.
$this->assertEqual(url('node', array('absolute' => TRUE)), $this->getUrl());
}
/**
* Test the translate tab for a single checkout.
*/
function testTranslateTabMultipeCheckout() {
// Create a user that is allowed to translate nodes.
$translater = $this->drupalCreateUser(array('translate content', 'create translation jobs', 'submit translation jobs', 'accept translation jobs'));
$this->drupalLogin($translater);
// Create an english source node.
$node = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
// Go to the translate tab.
$this->drupalGet('node/' . $node->nid);
$this->clickLink('Translate');
// Assert some basic strings on that page.
$this->assertText(t('Translations of @title', array('@title' => $node->title)));
$this->assertText(t('Pending Translations'));
// Request a translation for german.
$edit = array(
'languages[de]' => TRUE,
'languages[es]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
// Verify that we are on the translate tab.
$this->assertText(t('2 jobs need to be checked out.'));
// Submit all jobs.
$this->assertText($node->title);
$this->drupalPost(NULL, array(), t('Submit to translator and continue'));
$this->assertText($node->title);
$this->drupalPost(NULL, array(), t('Submit to translator'));
// Make sure that we're back on the translate tab.
$this->assertEqual(url('node/' . $node->nid . '/translate', array('absolute' => TRUE)), $this->getUrl());
$this->assertText(t('Test translation created.'));
$this->assertText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node->title, '@language' => t('Spanish'))));
// Review.
$this->clickLink(t('Needs review'));
// @todo Review job throuh the UI.
$items = tmgmt_job_item_load_latest('node', 'node', $node->nid, 'en');
$items['de']->acceptTranslation();
$items['es']->acceptTranslation();
// Translated nodes should now be listed and be clickable.
$this->drupalGet('node/' . $node->nid . '/translate');
$this->clickLink('de_' . $node->title);
// Translated nodes should now be listed and be clickable.
$this->drupalGet('node/' . $node->nid . '/translate');
$this->clickLink('es_' . $node->title);
}
/**
* Test the translate tab for a single checkout.
*/
function testTranslateTabAutomatedCheckout() {
// Hide settings on the test translator.
$default_translator = tmgmt_translator_load('test_translator');
$default_translator->settings = array(
'expose_settings' => FALSE,
);
$default_translator->save();
// Create a user that is allowed to translate nodes.
$translater = $this->drupalCreateUser(array('translate content', 'create translation jobs', 'submit translation jobs', 'accept translation jobs'));
$this->drupalLogin($translater);
// Create an english source node.
$node = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
// Go to the translate tab.
$this->drupalGet('node/' . $node->nid);
$this->clickLink('Translate');
// Assert some basic strings on that page.
$this->assertText(t('Translations of @title', array('@title' => $node->title)));
$this->assertText(t('Pending Translations'));
// Request a translation for german.
$edit = array(
'languages[de]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
// Verify that we are on the translate tab.
$this->assertNoText(t('One job needs to be checked out.'));
// Make sure that we're back on the translate tab.
$this->assertEqual(url('node/' . $node->nid . '/translate', array('absolute' => TRUE)), $this->getUrl());
$this->assertText(t('Test translation created.'));
$this->assertText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node->title, '@language' => t('German'))));
// Review.
$this->clickLink(t('Needs review'));
// @todo Review job throuh the UI.
$items = tmgmt_job_item_load_latest('node', 'node', $node->nid, 'en');
$items['de']->acceptTranslation();
// German node should now be listed and be clickable.
$this->drupalGet('node/' . $node->nid . '/translate');
$this->clickLink('de_' . $node->title);
}
/**
* Test the translate tab for a single checkout.
*/
function testTranslateTabDisabledQuickCheckout() {
variable_set('tmgmt_quick_checkout', FALSE);
// Hide settings on the test translator.
$default_translator = tmgmt_translator_load('test_translator');
$default_translator->settings = array(
'expose_settings' => FALSE,
);
$default_translator->save();
// Create a user that is allowed to translate nodes.
$translater = $this->drupalCreateUser(array('translate content', 'create translation jobs', 'submit translation jobs', 'accept translation jobs'));
$this->drupalLogin($translater);
// Create an english source node.
$node = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
// Go to the translate tab.
$this->drupalGet('node/' . $node->nid);
$this->clickLink('Translate');
// Assert some basic strings on that page.
$this->assertText(t('Translations of @title', array('@title' => $node->title)));
$this->assertText(t('Pending Translations'));
// Request a translation for german.
$edit = array(
'languages[de]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
// Verify that we are on the translate tab.
$this->assertText(t('One job needs to be checked out.'));
$this->assertText($node->title);
// Submit.
$this->drupalPost(NULL, array(), t('Submit to translator'));
// Make sure that we're back on the translate tab.
$this->assertEqual(url('node/' . $node->nid . '/translate', array('absolute' => TRUE)), $this->getUrl());
$this->assertText(t('Test translation created.'));
$this->assertText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node->title, '@language' => t('German'))));
// Review.
$this->clickLink(t('Needs review'));
// @todo Review job throuh the UI.
$items = tmgmt_job_item_load_latest('node', 'node', $node->nid, 'en');
$items['de']->acceptTranslation();
// German node should now be listed and be clickable.
$this->drupalGet('node/' . $node->nid . '/translate');
$this->clickLink('de_' . $node->title);
}
/**
* Test the node source specific cart functionality.
*/
function testCart() {
$nodes = array();
for ($i = 0; $i < 4; $i++) {
$nodes[] = $this->createNode('page');
}
$this->loginAsAdmin(array_merge($this->translator_permissions, array('translate content')));
// Test the source overview.
$this->drupalPost('admin/tmgmt/sources/node', array(
'views_bulk_operations[0]' => TRUE,
'views_bulk_operations[1]' => TRUE,
), t('Add to cart'));
$this->drupalGet('admin/tmgmt/cart');
$this->assertText($nodes[0]->title);
$this->assertText($nodes[1]->title);
// Test the translate tab.
$this->drupalGet('node/' . $nodes[3]->nid . '/translate');
$this->assertRaw(t('There are @count items in the <a href="@url">translation cart</a>.',
array('@count' => 2, '@url' => url('admin/tmgmt/cart'))));
$this->drupalPost(NULL, array(), t('Add to cart'));
$this->assertRaw(t('@count content source was added into the <a href="@url">cart</a>.', array('@count' => 1, '@url' => url('admin/tmgmt/cart'))));
$this->assertRaw(t('There are @count items in the <a href="@url">translation cart</a> including the current item.',
array('@count' => 3, '@url' => url('admin/tmgmt/cart'))));
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* Field handler which shows all jobs which contains a node.
*
* @TODO: This could probably abstracted into a more generic handler,
* or even api function.
*/
class tmgmt_node_ui_handler_field_jobs extends views_handler_field_prerender_list {
function pre_render(&$values) {
$nids = array();
foreach ($values as $row) {
$nid = $this->get_value($row);
$nids[] = $nid;
}
$select = db_select('tmgmt_job', 'tj');
$select->join('tmgmt_job_item', 'tji', "tj.id = %alias.tjid");
$select->join('node', 'n', "tji.item_type = 'node' AND tji.plugin = 'node' AND tji.item_id = node.nid");
$select->addField('n', 'nid');
$select->addExpression('MAX(tj.id)');
dpq($select);
}
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* Field handler to display the status of all languages.
*
* @ingroup views_field_handlers
*/
class tmgmt_node_handler_field_translation_language_status extends views_handler_field {
/**
* @var views_plugin_query_default
*/
var $query;
/**
* @var array
*/
public $language_items;
/**
* Array of active job items.
*
* @var array
*/
public $active_job_items = array();
function init(&$view, &$options) {
parent::init($view, $options);
$this->view->init_style();
$this->additional_fields['nid'] = 'nid';
/**
* Dynamically add new fields so they are used
*/
$languages = language_list('language');
foreach ($languages as $langcode => $lang_info) {
$handler = views_get_handler($this->table, $this->field . '_single', 'field');
if ($handler) {
$id = $options['id'] . '_single_' . $langcode;
$this->view->display_handler->handlers['field'][$id] = $handler;
$info = array(
'id' => $id,
'table' => $this->table,
'field' => $this->field . '_single',
'label' => $lang_info->name,
);
$handler->langcode = $langcode;
$handler->main_field = $options['id'];
$handler->init($this->view, $info);
$this->language_handlers[$langcode] = $handler;
}
}
}
function pre_render(&$values) {
$nids = array();
foreach ($values as $value) {
$tnid = $this->get_value($value);
$tnid = !empty($tnid) ? $tnid : $this->get_value($value, 'nid');
$this->active_job_items[$tnid] = tmgmt_job_item_load_latest('node', 'node', $tnid, $value->node_language);
$nids[] = $tnid;
}
if ($nodes = node_load_multiple($nids)) {
$result = db_select('node', 'n')
->fields('n', array('tnid', 'language', 'translate'))
->condition('tnid', $nids)
->execute()
->fetchAll();
$this->language_items = array();
foreach ($result as $tnode) {
// The translate flag is set if the translation node is outdated, revert
// to have FALSE for outdated translations.
$this->language_items[$tnode->tnid][$tnode->language] = !$tnode->translate;
}
}
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* @todo What is this?
*/
class tmgmt_node_handler_field_translation_language_status_single extends views_handler_field {
/**
* @var tmgmt_node_handler_field_translation_language_status
*/
var $main_handler;
/**
* @var string
*/
var $langcode;
function init(&$view, &$options) {
parent::init($view, $options);
$this->additional_fields['nid'] = array(
'table' => 'node',
'field' => 'nid',
);
}
function render($values) {
$nid = $values->nid;
$langcode = $this->langcode;
// Check if this is the source language.
if ($langcode == $values->node_language) {
$translation_status = 'original';
}
// Check if there is a translation.
elseif (!isset($this->view->field[$this->main_field]->language_items[$nid][$langcode])) {
$translation_status = 'missing';
}
// Check if the translation is outdated.
elseif (!$this->view->field[$this->main_field]->language_items[$nid][$langcode]) {
$translation_status = 'outofdate';
}
else {
$translation_status = 'current';
}
$job_item = NULL;
if (isset($this->view->field[$this->main_field]->active_job_items[$nid][$langcode])) {
$job_item = $this->view->field[$this->main_field]->active_job_items[$nid][$langcode];
}
return theme('tmgmt_ui_translation_language_status_single', array(
'translation_status' => $translation_status,
'job_item' => $job_item,
));
}
function query() {
$this->add_additional_fields();
}
}

View File

@@ -0,0 +1,108 @@
<?php
/**
* @file
* Definition of tmgmt_node_handler_filter_missing_translation.
*/
/**
* Filter by language.
*
* @ingroup views_filter_handlers
*/
class tmgmt_node_handler_filter_missing_translation extends views_handler_filter {
/**
* The target status to use for the query.
*
* @var string
*/
protected $target_status = 'untranslated_or_outdated';
/**
* {@inheritdoc}
*/
function query() {
$this->ensure_my_table();
// Don't do anything if no language was selected.
if (!$this->value) {
return;
}
$join = new views_join();
$join->definition['left_table'] = $this->table_alias;
$join->definition['left_field'] = $this->real_field;
$join->definition['table'] = 'node';
$join->definition['field'] = 'tnid';
$join->definition['type'] = 'LEFT';
$join->construct();
$join->extra = array(array(
'field' => 'language',
'value' => $this->value,
));
$table_alias = $this->query->add_table('node', $this->relationship, $join);
$this->query->add_where_expression($this->options['group'], "{$this->table_alias}.language != :language", array(':language' => $this->value));
if ($this->target_status == 'untranslated_or_outdated') {
$this->query->add_where_expression($this->options['group'], "($table_alias.nid IS NULL OR {$this->table_alias}.translate = 1)");
}
elseif ($this->target_status == 'outdated') {
$this->query->add_where_expression($this->options['group'], "{$this->table_alias}.translate = 1");
}
elseif ($this->target_status == 'untranslated') {
$this->query->add_where_expression($this->options['group'], "$table_alias.nid IS NULL");
}
}
/**
* {@inheritdoc}
*/
function value_form(&$form, &$form_state) {
$options = array();
foreach (language_list() as $langcode => $language) {
$options[$langcode] = $language->name;
}
$identifier = $this->options['expose']['identifier'];
$form['value'][$identifier] = array(
'#type' => 'select',
'#options' => $options,
'#empty_option' => t('Any'),
'#id' => 'tmgmt_node_missing_target_language',
'#element_validate' => array('tmgmt_node_views_exposed_target_language_validate'),
);
// Attach css to style the target_status element inline.
$form['#attached']['css'][] = drupal_get_path('module', 'tmgmt_node_ui') . '/tmgmt_node_ui.source_overview.css';
$form['value']['target_status'] = array(
'#type' => 'select',
'#title' => t('Target status'),
'#options' => array(
'untranslated_or_outdated' => t('Untranslated or outdated'),
'untranslated' => t('Untranslated'),
'outdated' => t('Outdated'),
),
'#states' => array(
'invisible' => array(
':input[id="tmgmt_node_missing_target_language"]' => array('value' => ''),
),
),
);
}
/**
* {@inheritdoc}
*/
function accept_exposed_input($input) {
$return = parent::accept_exposed_input($input);
if ($return && isset($input['target_status'])) {
$this->target_status = $input['target_status'];
}
return $return;
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* @file
* Contains tmgmt_node_ui_handler_filter_node_translatable_types.
*/
/**
* Limits node types to those enabled for content translation.
*/
class tmgmt_node_ui_handler_filter_node_translatable_types extends views_handler_filter {
/**
* {@inheritdoc}
*/
function query() {
$this->ensure_my_table();
$valid_types = array_keys(tmgmt_source_translatable_item_types('node'));
if ($valid_types) {
$this->query->add_where($this->options['group'], "$this->table_alias.$this->real_field", array_values($valid_types), 'IN');
}
else {
// There are no valid translatable node types, do not return any results.
$this->query->add_where_expression($this->options['group'], '1 = 0');
}
}
/**
* {@inheritdoc}
*/
function admin_summary() { }
}

View File

@@ -0,0 +1,140 @@
<?php
/**
* @file
* Please supply a file description.
*/
class TMGMTNodeSourceViewsController extends TMGMTDefaultSourceViewsController {
/**
* {@inheritdoc}
*/
public function views_data() {
// Relationships between job items and nodes.
$data['tmgmt_job_item']['job_item_to_node'] = array(
'title' => t('Content'),
'help' => t('Content that is associated with this job item.'),
'real field' => 'item_id',
'relationship' => array(
'label' => t('Content'),
'base' => 'node',
'base field' => 'vid',
'relationship field' => 'item_id',
'extra' => array(
array(
'table' => 'tmgmt_job_item',
'field' => 'item_type',
'operator' => '=',
'value' => 'node',
),
array(
'table' => 'tmgmt_job_item',
'field' => 'plugin',
'operator' => '=',
'value' => 'node',
),
),
),
);
$data['node']['node_to_job_item'] = array(
'title' => t('Translation job item'),
'help' => t('Job items of this node.'),
'relationship' => array(
'real field' => 'vid',
'label' => t('Translation job item'),
'base' => 'tmgmt_job_item',
'base field' => 'item_id',
'extra' => array(
array(
'field' => 'item_type',
'operator' => '=',
'value' => 'node',
),
array(
'field' => 'plugin',
'operator' => '=',
'value' => 'node',
),
),
),
);
$data['node']['tmgmt_translatable_types_all'] = array(
'group' => t('Content translation'),
'title' => t('All translatable types'),
'help' => t('Enforces that only nodes from node types which are translatable are '),
'filter' => array(
'handler' => 'tmgmt_node_ui_handler_filter_node_translatable_types',
'real field' => 'type',
),
);
$data['node']['tmgmt_node_missing_translation'] = array(
'group' => t('Content translation'),
'title' => t('Missing translation'),
'help' => t('Enables the search for nodes with missing translation ofr the specified language'),
'filter' => array(
'handler' => 'tmgmt_node_handler_filter_missing_translation',
'real field' => 'nid',
),
);
$data['node']['tmgmt_jobs'] = array(
'title' => t('Translation jobs'),
'help' => t('Shows all translation jobs which contains this node'),
'field' => array(
'handler' => 'tmgmt_node_ui_handler_field_jobs',
'real field' => 'nid',
),
);
$data['node']['tmgmt_job_item'] = array(
'title' => t('Job item'),
'real field' => 'vid',
'relationship' => array(
'title' => t('Translation job item'),
'label' => t('Translation job item'),
'base' => 'tmgmt_job_item',
'base field' => 'item_id',
'extra' => array(
array(
'field' => 'item_type',
'operator' => '=',
'value' => 'node',
),
array(
'field' => 'plugin',
'operator' => '=',
'value' => 'node',
),
),
),
);
$data['node']['translation_language_status'] = array(
'group' => t('Content translation'),
'title' => t('All translation languages'),
'help' => t('Display all target lanuages.'),
'real field' => 'tnid',
'field' => array(
'handler' => 'tmgmt_node_handler_field_translation_language_status',
),
);
$data['node']['translation_language_status_single'] = array(
'title' => t('All translation languages (single)'),
'help' => t("Don't use this in the user interface."),
'field' => array(
'handler' => 'tmgmt_node_handler_field_translation_language_status_single',
),
);
$data['node']['tmgmt_translatable_types_select'] = array(
'group' => t('Content translation'),
'title' => t('Select translatable content types'),
'help' => t('Allows to filter on specific translatable types.'),
'filter' => array(
'handler' => 'views_handler_filter_in_operator',
'real field' => 'type',
'options callback' => 'tmgmt_source_translatable_item_types',
'options arguments' => array($this->pluginType),
),
);
return $data;
}
}

View File

@@ -0,0 +1,340 @@
<?php
$view = new view();
$view->name = 'tmgmt_node_source_overview';
$view->description = 'Node source overview for bulk operations.';
$view->tag = 'Translation Management';
$view->base_table = 'node';
$view->human_name = 'Node Source Overview';
$view->core = 7;
$view->api_version = '3.0';
$view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
/* Display: Master */
$handler = $view->new_display('default', 'Master', 'default');
$handler->display->display_options['title'] = 'Content overview';
$handler->display->display_options['use_more_always'] = FALSE;
$handler->display->display_options['group_by'] = TRUE;
$handler->display->display_options['access']['type'] = 'perm';
$handler->display->display_options['access']['perm'] = 'create translation jobs';
$handler->display->display_options['cache']['type'] = 'none';
$handler->display->display_options['query']['type'] = 'views_query';
$handler->display->display_options['query']['options']['query_comment'] = FALSE;
$handler->display->display_options['exposed_form']['type'] = 'basic';
$handler->display->display_options['exposed_form']['options']['submit_button'] = 'Search';
$handler->display->display_options['pager']['type'] = 'full';
$handler->display->display_options['pager']['options']['items_per_page'] = variable_get('tmgmt_source_list_limit', 20);
$handler->display->display_options['pager']['options']['offset'] = '0';
$handler->display->display_options['pager']['options']['id'] = '0';
$handler->display->display_options['pager']['options']['quantity'] = '9';
$handler->display->display_options['style_plugin'] = 'table';
$handler->display->display_options['style_options']['columns'] = array(
'title' => 'title',
);
$handler->display->display_options['style_options']['default'] = '-1';
$handler->display->display_options['style_options']['info'] = array(
'title' => array(
'sortable' => 0,
'default_sort_order' => 'asc',
'align' => '',
'separator' => '',
'empty_column' => 0,
),
);
/* No results behavior: Global: Text area */
$handler->display->display_options['empty']['area']['id'] = 'area';
$handler->display->display_options['empty']['area']['table'] = 'views';
$handler->display->display_options['empty']['area']['field'] = 'area';
$handler->display->display_options['empty']['area']['content'] = 'There are no nodes that match the specified filter criteria.';
$handler->display->display_options['empty']['area']['format'] = 'filtered_html';
/* Relationship: User */
$handler->display->display_options['relationships']['uid']['id'] = 'uid';
$handler->display->display_options['relationships']['uid']['table'] = 'node';
$handler->display->display_options['relationships']['uid']['field'] = 'uid';
$handler->display->display_options['relationships']['uid']['ui_name'] = 'User';
$handler->display->display_options['relationships']['uid']['label'] = 'User';
/* Field: Bulk operations */
$handler->display->display_options['fields']['views_bulk_operations']['id'] = 'views_bulk_operations';
$handler->display->display_options['fields']['views_bulk_operations']['table'] = 'node';
$handler->display->display_options['fields']['views_bulk_operations']['field'] = 'views_bulk_operations';
$handler->display->display_options['fields']['views_bulk_operations']['ui_name'] = 'Bulk operations';
$handler->display->display_options['fields']['views_bulk_operations']['label'] = '<!--views-bulk-operations-select-all-->';
$handler->display->display_options['fields']['views_bulk_operations']['element_label_colon'] = FALSE;
$handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['display_type'] = '1';
$handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['enable_select_all_pages'] = 1;
$handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['force_single'] = 0;
$handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['entity_load_capacity'] = '10';
$handler->display->display_options['fields']['views_bulk_operations']['vbo_operations'] = array(
'rules_component::tmgmt_node_ui_tmgmt_nodes_add_items_to_cart' => array(
'selected' => 1,
'skip_confirmation' => 1,
'override_label' => 0,
'label' => '',
),
'action::node_assign_owner_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::views_bulk_operations_delete_item' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::views_bulk_operations_script_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::node_make_sticky_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::node_make_unsticky_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::views_bulk_operations_modify_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
'settings' => array(
'show_all_tokens' => 1,
'display_values' => array(
'_all_' => '_all_',
),
),
),
'action::views_bulk_operations_argument_selector_action' => array(
'selected' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
'settings' => array(
'url' => '',
),
),
'action::node_promote_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::node_publish_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::node_unpromote_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'rules_component::tmgmt_node_ui_request_translation' => array(
'selected' => 0,
'skip_confirmation' => 1,
'override_label' => 0,
'label' => '',
),
'action::tmgmt_node_ui_checkout_multiple_action' => array(
'selected' => 1,
'skip_confirmation' => 1,
'override_label' => 0,
'label' => '',
),
'action::node_save_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::system_send_email_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::node_unpublish_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::node_unpublish_by_keyword_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
);
/* Field: Title */
$handler->display->display_options['fields']['title']['id'] = 'title';
$handler->display->display_options['fields']['title']['table'] = 'node';
$handler->display->display_options['fields']['title']['field'] = 'title';
$handler->display->display_options['fields']['title']['ui_name'] = 'Title';
$handler->display->display_options['fields']['title']['label'] = 'Title (in source language)';
$handler->display->display_options['fields']['title']['alter']['word_boundary'] = FALSE;
$handler->display->display_options['fields']['title']['alter']['ellipsis'] = FALSE;
/* Field: Type */
$handler->display->display_options['fields']['type']['id'] = 'type';
$handler->display->display_options['fields']['type']['table'] = 'node';
$handler->display->display_options['fields']['type']['field'] = 'type';
$handler->display->display_options['fields']['type']['ui_name'] = 'Type';
/* Field: All translation languages */
$handler->display->display_options['fields']['translation_language_status_1']['id'] = 'translation_language_status_1';
$handler->display->display_options['fields']['translation_language_status_1']['table'] = 'node';
$handler->display->display_options['fields']['translation_language_status_1']['field'] = 'translation_language_status';
$handler->display->display_options['fields']['translation_language_status_1']['ui_name'] = 'All translation languages';
$handler->display->display_options['fields']['translation_language_status_1']['exclude'] = TRUE;
/* Field: Author */
$handler->display->display_options['fields']['name']['id'] = 'name';
$handler->display->display_options['fields']['name']['table'] = 'users';
$handler->display->display_options['fields']['name']['field'] = 'name';
$handler->display->display_options['fields']['name']['relationship'] = 'uid';
$handler->display->display_options['fields']['name']['ui_name'] = 'Author';
$handler->display->display_options['fields']['name']['label'] = 'Author';
/* Field: Updated date */
$handler->display->display_options['fields']['changed']['id'] = 'changed';
$handler->display->display_options['fields']['changed']['table'] = 'node';
$handler->display->display_options['fields']['changed']['field'] = 'changed';
$handler->display->display_options['fields']['changed']['ui_name'] = 'Updated date';
$handler->display->display_options['fields']['changed']['date_format'] = 'short';
/* Sort criterion: Post date */
$handler->display->display_options['sorts']['created']['id'] = 'created';
$handler->display->display_options['sorts']['created']['table'] = 'node';
$handler->display->display_options['sorts']['created']['field'] = 'created';
$handler->display->display_options['sorts']['created']['ui_name'] = 'Post date';
$handler->display->display_options['sorts']['created']['order'] = 'DESC';
/* Filter criterion: Content: Title */
$handler->display->display_options['filters']['title']['id'] = 'title';
$handler->display->display_options['filters']['title']['table'] = 'node';
$handler->display->display_options['filters']['title']['field'] = 'title';
$handler->display->display_options['filters']['title']['operator'] = 'word';
$handler->display->display_options['filters']['title']['group'] = 1;
$handler->display->display_options['filters']['title']['exposed'] = TRUE;
$handler->display->display_options['filters']['title']['expose']['operator_id'] = 'title_op';
$handler->display->display_options['filters']['title']['expose']['label'] = 'Node title';
$handler->display->display_options['filters']['title']['expose']['operator'] = 'title_op';
$handler->display->display_options['filters']['title']['expose']['identifier'] = 'title';
/* Filter criterion: Published */
$handler->display->display_options['filters']['status']['id'] = 'status';
$handler->display->display_options['filters']['status']['table'] = 'node';
$handler->display->display_options['filters']['status']['field'] = 'status';
$handler->display->display_options['filters']['status']['ui_name'] = 'Published';
$handler->display->display_options['filters']['status']['value'] = '1';
$handler->display->display_options['filters']['status']['group'] = 1;
$handler->display->display_options['filters']['status']['exposed'] = TRUE;
$handler->display->display_options['filters']['status']['expose']['operator_id'] = '';
$handler->display->display_options['filters']['status']['expose']['label'] = 'Published';
$handler->display->display_options['filters']['status']['expose']['operator'] = 'status_op';
$handler->display->display_options['filters']['status']['expose']['identifier'] = 'status';
$handler->display->display_options['filters']['status']['expose']['required'] = TRUE;
/* Filter criterion: Source translation */
$handler->display->display_options['filters']['source_translation']['id'] = 'source_translation';
$handler->display->display_options['filters']['source_translation']['table'] = 'node';
$handler->display->display_options['filters']['source_translation']['field'] = 'source_translation';
$handler->display->display_options['filters']['source_translation']['ui_name'] = 'Source translation';
$handler->display->display_options['filters']['source_translation']['operator'] = '1';
$handler->display->display_options['filters']['source_translation']['group'] = 1;
/* Filter criterion: Content: Language */
$handler->display->display_options['filters']['language']['id'] = 'language';
$handler->display->display_options['filters']['language']['table'] = 'node';
$handler->display->display_options['filters']['language']['field'] = 'language';
$handler->display->display_options['filters']['language']['operator'] = 'not in';
$handler->display->display_options['filters']['language']['value'] = array(
'und' => 'und',
);
$handler->display->display_options['filters']['language']['group'] = 1;
/* Filter criterion: Content: Language */
$handler->display->display_options['filters']['language_1']['id'] = 'language_1';
$handler->display->display_options['filters']['language_1']['table'] = 'node';
$handler->display->display_options['filters']['language_1']['field'] = 'language';
$handler->display->display_options['filters']['language_1']['group'] = 1;
$handler->display->display_options['filters']['language_1']['exposed'] = TRUE;
$handler->display->display_options['filters']['language_1']['expose']['operator_id'] = 'language_1_op';
$handler->display->display_options['filters']['language_1']['expose']['label'] = 'Source language';
$handler->display->display_options['filters']['language_1']['expose']['operator'] = 'language_1_op';
$handler->display->display_options['filters']['language_1']['expose']['identifier'] = 'language_1';
/* Filter criterion: Content translation: Select translatable content types */
$handler->display->display_options['filters']['tmgmt_translatable_types_select']['id'] = 'tmgmt_translatable_types_select';
$handler->display->display_options['filters']['tmgmt_translatable_types_select']['table'] = 'node';
$handler->display->display_options['filters']['tmgmt_translatable_types_select']['field'] = 'tmgmt_translatable_types_select';
$handler->display->display_options['filters']['tmgmt_translatable_types_select']['exposed'] = TRUE;
$handler->display->display_options['filters']['tmgmt_translatable_types_select']['expose']['operator_id'] = 'tmgmt_translatable_types_select_op';
$handler->display->display_options['filters']['tmgmt_translatable_types_select']['expose']['label'] = 'Content type';
$handler->display->display_options['filters']['tmgmt_translatable_types_select']['expose']['operator'] = 'tmgmt_translatable_types_select_op';
$handler->display->display_options['filters']['tmgmt_translatable_types_select']['expose']['identifier'] = 'tmgmt_translatable_types_select';
/* Filter criterion: Content translation: All translatable types */
$handler->display->display_options['filters']['tmgmt_translatable_types_all']['id'] = 'tmgmt_translatable_types_all';
$handler->display->display_options['filters']['tmgmt_translatable_types_all']['table'] = 'node';
$handler->display->display_options['filters']['tmgmt_translatable_types_all']['field'] = 'tmgmt_translatable_types_all';
/* Filter criterion: Content translation: Missing translation */
$handler->display->display_options['filters']['tmgmt_node_missing_translation']['id'] = 'tmgmt_node_missing_translation';
$handler->display->display_options['filters']['tmgmt_node_missing_translation']['table'] = 'node';
$handler->display->display_options['filters']['tmgmt_node_missing_translation']['field'] = 'tmgmt_node_missing_translation';
$handler->display->display_options['filters']['tmgmt_node_missing_translation']['exposed'] = TRUE;
$handler->display->display_options['filters']['tmgmt_node_missing_translation']['expose']['operator_id'] = 'tmgmt_node_missing_translation_op';
$handler->display->display_options['filters']['tmgmt_node_missing_translation']['expose']['label'] = 'Target language';
$handler->display->display_options['filters']['tmgmt_node_missing_translation']['expose']['operator'] = 'tmgmt_node_missing_translation_op';
$handler->display->display_options['filters']['tmgmt_node_missing_translation']['expose']['identifier'] = 'tmgmt_node_missing_translation';
/* Display: Page */
$handler = $view->new_display('page', 'Page', 'page');
$handler->display->display_options['path'] = 'admin/tmgmt/sources/node';
$handler->display->display_options['menu']['type'] = 'tab';
$handler->display->display_options['menu']['title'] = 'Content';
$handler->display->display_options['menu']['weight'] = -20;
$handler->display->display_options['menu']['context'] = 0;
$translatables['tmgmt_node_source_overview'] = array(
t('Master'),
t('Content overview'),
t('more'),
t('Search'),
t('Reset'),
t('Sort by'),
t('Asc'),
t('Desc'),
t('Items per page'),
t('- All -'),
t('Offset'),
t('« first'),
t(' previous'),
t('next '),
t('last »'),
t('There are no nodes that match the specified filter criteria.'),
t('User'),
t('<!--views-bulk-operations-select-all-->'),
t('Title (in source language)'),
t('Type'),
t('All translation languages'),
t('Author'),
t('Updated date'),
t('Node title'),
t('Published'),
t('Source language'),
t('Content type'),
t('Page'),
);

View File

@@ -0,0 +1,6 @@
<p>one paragraph with special characters: äöüľščťžýáíéäňú©«®™»</p>
<p>one paragraph with a <br />break line</p>
<p>one paragraph with html entities: &amp;&lt;&gt;</p>
<p>and here we have some link <a href="http://example.com">break line</a></p>
<p>one paragraph with an <img src="not-existing.gif" alt="not existing image" title="not existing image" />image</p>
<p>hello <span class="green">world</span> this is <span class="red">simple html</span></p><div>nested 1<div>nested 2<div>nested 3</div></div></div>

View File

@@ -0,0 +1,245 @@
<?php
/*
* @file
* Contains tests for Translation management
*/
/**
* Utility test case class with helper methods to create entities and their
* fields with populated translatable content. Extend this class if you create
* tests in which you need Drupal entities and/or fields.
*/
abstract class TMGMTEntityTestCaseUtility extends TMGMTBaseTestCase {
public $field_names = array();
/**
* Creates node type with several text fields with different cardinality.
*
* Internally it calls TMGMTEntityTestCaseUtility::attachFields() to create
* and attach fields to newly created bundle. You can than use
* $this->field_names['node']['YOUR_BUNDLE_NAME'] to access them.
*
* @param string $machine_name
* Machine name of the node type.
* @param string $human_name
* Human readable name of the node type.
* @param int $language_content_type
* Either 0 (disabled), 1 (language enabled but no translations),
* TRANSLATION_ENABLED or ENTITY_TRANSLATION_ENABLED.
* pparam bool $attach_fields
* (optional) If fields with the same translatability should automatically
* be attached to the node type.
*/
function createNodeType($machine_name, $human_name, $language_content_type = 0, $attach_fields = TRUE) {
// Create new bundle.
$type = array(
'type' => $machine_name,
'name' => $human_name,
'base' => 'node_content',
'description' => '',
'custom' => 1,
'modified' => 1,
'locked' => 0,
);
$type = node_type_set_defaults($type);
node_type_save($type);
node_add_body_field($type);
node_types_rebuild();
// Set content type to be translatable as specified by
// $language_content_type.
$edit = array();
$edit['language_content_type'] = $language_content_type;
$this->drupalPost('admin/structure/types/manage/' . $machine_name, $edit, t('Save content type'));
$translatable = FALSE;
if (defined('ENTITY_TRANSLATION_ENABLED') && $language_content_type == ENTITY_TRANSLATION_ENABLED) {
$translatable = TRUE;
}
// Push in also the body field.
$this->field_names['node'][$machine_name][] = 'body';
if ($attach_fields) {
$this->attachFields('node', $machine_name, $translatable);
}
// Change body field to be translatable.
$body = field_info_field('body');
$body['translatable'] = $translatable;
field_update_field($body);
}
/**
* Creates taxonomy vocabulary with custom fields.
*
* To create and attach fields it internally calls
* TMGMTEntityTestCaseUtility::attachFields(). You can than access these
* fields calling $this->field_names['node']['YOUR_BUNDLE_NAME'].
*
* @param string $machine_name
* Vocabulary machine name.
* @param string $human_name
* Vocabulary human readable name.
* @param bool|array $fields_translatable
* Flag or definition array to determine which or all fields should be
* translatable.
*
* @return stdClass
* Created vocabulary object.
*/
function createTaxonomyVocab($machine_name, $human_name, $fields_translatable = TRUE) {
$vocabulary = new stdClass();
$vocabulary->name = $human_name;
$vocabulary->machine_name = $machine_name;
taxonomy_vocabulary_save($vocabulary);
$this->attachFields('taxonomy_term', $vocabulary->machine_name, $fields_translatable);
return $vocabulary;
}
/**
* Creates fields of type text and text_with_summary of different cardinality.
*
* It will attach created fields to provided entity name and bundle.
*
* Field names will be stored in $this->field_names['entity']['bundle']
* through which you can access them.
*
* @param string $entity_name
* Entity name to which fields should be attached.
* @param string $bundle
* Bundle name to which fields should be attached.
* @param bool|array $translatable
* Flag or definition array to determine which or all fields should be
* translatable.
*/
function attachFields($entity_name, $bundle, $translatable = TRUE) {
// Create several text fields.
$field_types = array('text', 'text_with_summary');
for ($i = 0 ; $i <= 5; $i++) {
$field_type = $field_types[array_rand($field_types, 1)];
$field_name = drupal_strtolower($this->randomName());
// Create a field.
$field = array(
'field_name' => $field_name,
'type' => $field_type,
'cardinality' => mt_rand(1, 5),
'translatable' => is_array($translatable) && isset($translatable[$i]) ? $translatable[$i] : (boolean) $translatable,
);
field_create_field($field);
// Create an instance of the previously created field.
$instance = array(
'field_name' => $field_name,
'entity_type' => $entity_name,
'bundle' => $bundle,
'label' => $this->randomName(10),
'description' => $this->randomString(30),
'widget' => array(
'type' => $field_type == 'text' ? 'text_textfield' : 'text_textarea_with_summary',
'label' => $this->randomString(10),
),
);
field_create_instance($instance);
// Store field names in case there are needed outside this method.
$this->field_names[$entity_name][$bundle][] = $field_name;
}
}
/**
* Creates a node of a given bundle.
*
* It uses $this->field_names to populate content of attached fields.
*
* @param string $bundle
* Node type name.
* @param string $sourcelang
* Source lang of the node to be created.
*
* @return object
* Newly created node object.
*/
function createNode($bundle, $sourcelang = 'en') {
$node = array(
'type' => $bundle,
'language' => $sourcelang,
// Ensure that the body field is defined for the node language.
'body' => array($sourcelang => array(0 => array())),
);
foreach ($this->field_names['node'][$bundle] as $field_name) {
$field_info = field_info_field($field_name);
$cardinality = $field_info['cardinality'] == FIELD_CARDINALITY_UNLIMITED ? 1 : $field_info['cardinality'];
$field_langcode = field_is_translatable('node', $field_info) ? $sourcelang : LANGUAGE_NONE;
// Create two deltas for each field.
for ($delta = 0; $delta <= $cardinality; $delta++) {
$node[$field_name][$field_langcode][$delta]['value'] = $this->randomName(20);
if ($field_info['type'] == 'text_with_summary') {
$node[$field_name][$field_langcode][$delta]['summary'] = $this->randomName(10);
}
}
}
return $this->drupalCreateNode($node);
}
/**
* Creates a taxonomy term of a given vocabulary.
*
* It uses $this->field_names to populate content of attached fields. You can
* access fields values using
* $this->field_names['taxonomy_term'][$vocabulary->machine_name].
*
* @param object $vocabulary
* Vocabulary object for which the term should be created.
*
* @param string $langcode
* The language code to be set as the entity source language.
*
* @return object
* Newly created node object.
*/
function createTaxonomyTerm($vocabulary, $langcode = 'en') {
// When an entity is being saved, the entity_translation module initializes
// a translation fetching the language from an entity. But the taxonomy
// terms have no entity language key, so its langcode will be the set to the
// default one.
/* @see entity_translation_field_attach_insert() */
/* @see EntityTranslationDefaultHandler::initTranslations() */
/* @see EntityTranslationDefaultHandler::getLanguage() */
$settings_variable_name = 'entity_translation_settings_taxonomy_term__' . $vocabulary->machine_name;
variable_set($settings_variable_name, array('default_language' => $langcode));
$term = new stdClass();
$term->name = $this->randomName();
$term->description = $this->randomName();
$term->vid = $vocabulary->vid;
foreach ($this->field_names['taxonomy_term'][$vocabulary->machine_name] as $field_name) {
$field_info = field_info_field($field_name);
$cardinality = $field_info['cardinality'] == FIELD_CARDINALITY_UNLIMITED ? 1 : $field_info['cardinality'];
$field_lang = $field_info['translatable'] ? $langcode : LANGUAGE_NONE;
// Create two deltas for each field.
for ($delta = 0; $delta <= $cardinality; $delta++) {
$term->{$field_name}[$field_lang][$delta]['value'] = $this->randomName(20);
if ($field_info['type'] == 'text_with_summary') {
$term->{$field_name}[$field_lang][$delta]['summary'] = $this->randomName(10);
}
}
}
taxonomy_term_save($term);
return taxonomy_term_load($term->tid);
}
}

View File

@@ -0,0 +1,219 @@
<?php
/*
* @file
* Contains tests for Translation management
*/
/**
* Base class for tests.
*/
class TMGMTBaseTestCase extends DrupalWebTestCase {
protected $profile = 'testing';
/**
* A default translator using the test translator.
*
* @var TMGMTTranslator
*/
protected $default_translator;
/**
* List of permissions used by loginAsAdmin().
*
* @var array
*/
protected $admin_permissions = array();
/**
* Drupal user object created by loginAsAdmin().
*
* @var object
*/
protected $admin_user = NULL;
/**
* List of permissions used by loginAsTranslator().
*
* @var array
*/
protected $translator_permissions = array();
/**
* Drupal user object created by loginAsTranslator().
*
* @var object
*/
protected $translator_user = NULL;
/**
* Overrides DrupalWebTestCase::setUp()
*/
function setUp() {
$modules = func_get_args();
if (isset($modules[0]) && is_array($modules[0])) {
$modules = $modules[0];
}
$modules = array_merge(array('entity', 'tmgmt', 'tmgmt_test'), $modules);
parent::setUp($modules);
$this->default_translator = tmgmt_translator_load('test_translator');
// Load default admin permissions.
$this->admin_permissions = array(
'administer languages',
'access administration pages',
'administer content types',
'administer tmgmt',
);
// Load default translator user permissions.
$this->translator_permissions = array(
'create translation jobs',
'submit translation jobs',
'accept translation jobs',
);
}
/**
* Will create a user with admin permissions and log it in.
*
* @param array $additional_permissions
* Additional permissions that will be granted to admin user.
* @param boolean $reset_permissions
* Flag to determine if default admin permissions will be replaced by
* $additional_permissions.
*
* @return object
* Newly created and logged in user object.
*/
function loginAsAdmin($additional_permissions = array(), $reset_permissions = FALSE) {
$permissions = $this->admin_permissions;
if ($reset_permissions) {
$permissions = $additional_permissions;
}
elseif (!empty($additional_permissions)) {
$permissions = array_merge($permissions, $additional_permissions);
}
$this->admin_user = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->admin_user);
return $this->admin_user;
}
/**
* Will create a user with translator permissions and log it in.
*
* @param array $additional_permissions
* Additional permissions that will be granted to admin user.
* @param boolean $reset_permissions
* Flag to determine if default admin permissions will be replaced by
* $additional_permissions.
*
* @return object
* Newly created and logged in user object.
*/
function loginAsTranslator($additional_permissions = array(), $reset_permissions = FALSE) {
$permissions = $this->translator_permissions;
if ($reset_permissions) {
$permissions = $additional_permissions;
}
elseif (!empty($additional_permissions)) {
$permissions = array_merge($permissions, $additional_permissions);
}
$this->translator_user = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->translator_user);
return $this->translator_user;
}
/**
* Creates, saves and returns a translator.
*
* @return TMGMTTranslator
*/
function createTranslator() {
$translator = new TMGMTTranslator();
$translator->name = strtolower($this->randomName());
$translator->label = $this->randomName();
$translator->plugin = 'test_translator';
$translator->settings = array(
'key' => $this->randomName(),
'another_key' => $this->randomName(),
);
$this->assertEqual(SAVED_NEW, $translator->save());
// Assert that the translator was assigned a tid.
$this->assertTrue($translator->tid > 0);
return $translator;
}
/**
* Creates, saves and returns a translation job.
*
* @return TMGMTJob
*/
function createJob($source = 'en', $target = 'de', $uid = 1) {
$job = tmgmt_job_create($source, $target, $uid);
$this->assertEqual(SAVED_NEW, $job->save());
// Assert that the translator was assigned a tid.
$this->assertTrue($job->tjid > 0);
return $job;
}
/**
* Sets the proper environment.
*
* Currently just adds a new language.
*
* @param string $langcode
* The language code.
*/
function setEnvironment($langcode) {
// Add the language.
locale_add_language($langcode);
}
/**
* Asserts job item language codes.
*
* @param TMGMTJobItem $job_item
* Job item to check.
* @param string $expected_source_lang
* Expected source language.
* @param array $actual_lang_codes
* Expected existing language codes (translations).
*/
function assertJobItemLangCodes(TMGMTJobItem $job_item, $expected_source_lang, array $actual_lang_codes) {
$this->assertEqual($job_item->getSourceLangCode(), $expected_source_lang);
$existing = $job_item->getExistingLangCodes();
sort($existing);
sort($actual_lang_codes);
$this->assertEqual($existing, $actual_lang_codes);
}
/**
* Adds languages as admin user and switches to a translator user.
*/
protected function createLanguagesLoginTranslator($permissions = NULL) {
// Login as admin to be able to set environment variables.
$this->loginAsAdmin();
$this->setEnvironment('de');
$this->setEnvironment('es');
$this->setEnvironment('el');
$base_permissions = array(
'access administration pages',
'create translation jobs',
'submit translation jobs',
);
$permissions = $permissions ? array_merge($permissions, $base_permissions) : $base_permissions;
// Login as translator only with limited permissions to run these tests.
$this->loginAsTranslator($permissions, TRUE);
}
}

View File

@@ -0,0 +1,532 @@
<?php
/*
* @file
* Contains tests for Translation management
*/
/**
* Basic CRUD tests.
*/
class TMGMTCRUDTestCase extends TMGMTBaseTestCase {
static function getInfo() {
return array(
'name' => 'CRUD tests',
'description' => 'Basic crud operations for jobs and translators',
'group' => 'Translation Management',
);
}
/**
* Test crud operations of translators.
*/
function testTranslators() {
$translator = $this->createTranslator();
$loaded_translator = tmgmt_translator_load($translator->tid);
$this->assertEqual($translator->name, $loaded_translator->name);
$this->assertEqual($translator->label, $loaded_translator->label);
$this->assertEqual($translator->settings, $loaded_translator->settings);
// Update the settings.
$translator->settings['new_key'] = $this->randomString();
$this->assertEqual(SAVED_UPDATED, $translator->save());
$loaded_translator = tmgmt_translator_load($translator->tid);
$this->assertEqual($translator->name, $loaded_translator->name);
$this->assertEqual($translator->label, $loaded_translator->label);
$this->assertEqual($translator->settings, $loaded_translator->settings);
// Delete the translator, make sure the translator is gone.
$translator->delete();
$this->assertFalse(tmgmt_translator_load($translator->tid));
}
/**
* Test crud operations of jobs.
*/
function testJobs() {
$job = $this->createJob();
$loaded_job = tmgmt_job_load($job->tjid);
$this->assertEqual($job->source_language, $loaded_job->source_language);
$this->assertEqual($job->target_language, $loaded_job->target_language);
// Assert that the created and changed information has been set to the
// default value.
$this->assertTrue($loaded_job->created > 0);
$this->assertTrue($loaded_job->changed > 0);
$this->assertEqual(0, $loaded_job->state);
// Update the settings.
$job->reference = 7;
$this->assertEqual(SAVED_UPDATED, $job->save());
$loaded_job = tmgmt_job_load($job->tjid);
$this->assertEqual($job->reference, $loaded_job->reference);
// Test the job items.
$item1 = $job->addItem('test_source', 'type', 5);
$item2 = $job->addItem('test_source', 'type', 4);
// Load and compare the items.
$items = $job->getItems();
$this->assertEqual(2, count($items));
$this->assertEqual($item1->plugin, $items[$item1->tjiid]->plugin);
$this->assertEqual($item1->item_type, $items[$item1->tjiid]->item_type);
$this->assertEqual($item1->item_id, $items[$item1->tjiid]->item_id);
$this->assertEqual($item2->plugin, $items[$item2->tjiid]->plugin);
$this->assertEqual($item2->item_type, $items[$item2->tjiid]->item_type);
$this->assertEqual($item2->item_id, $items[$item2->tjiid]->item_id);
// Delete the job and make sure it is gone.
$job->delete();
$this->assertFalse(tmgmt_job_load($job->tjid));
}
function testRemoteMappings() {
$data_key = '5][test_source][type';
$translator = $this->createTranslator();
$job = $this->createJob();
$job->translator = $translator->name;
$job->save();
$item1 = $job->addItem('test_source', 'type', 5);
$item2 = $job->addItem('test_source', 'type', 4);
$mapping_data = array(
'remote_identifier_2' => 'id12',
'remote_identifier_3' => 'id13',
'amount' => 1043,
'currency' => 'EUR',
);
$result = $item1->addRemoteMapping($data_key, 'id11', $mapping_data);
$this->assertEqual($result, SAVED_NEW);
$job_mappings = $job->getRemoteMappings();
$item_mappings = $item1->getRemoteMappings();
$job_mapping = array_shift($job_mappings);
$item_mapping = array_shift($item_mappings);
$_job = $job_mapping->getJob();
$this->assertEqual($job->tjid, $_job->tjid);
$_job = $item_mapping->getJob();
$this->assertEqual($job->tjid, $_job->tjid);
$_item1 = $item_mapping->getJobItem();
$this->assertEqual($item1->tjiid, $_item1->tjiid);
/**
* @var TMGMTRemoteController $remote_mapping_controller
*/
$remote_mapping_controller = entity_get_controller('tmgmt_remote');
$remote_mappings = $remote_mapping_controller->loadByRemoteIdentifier('id11', 'id12', 'id13');
$remote_mapping = array_shift($remote_mappings);
$this->assertEqual($remote_mapping->tjiid, $item1->tjiid);
$this->assertEqual($remote_mapping->amount, $mapping_data['amount']);
$this->assertEqual($remote_mapping->currency, $mapping_data['currency']);
$this->assertEqual(count($remote_mapping_controller->loadByRemoteIdentifier('id11')), 1);
$this->assertEqual(count($remote_mapping_controller->loadByRemoteIdentifier('id11', '')), 0);
$this->assertEqual(count($remote_mapping_controller->loadByRemoteIdentifier('id11', NULL, '')), 0);
$this->assertEqual(count($remote_mapping_controller->loadByRemoteIdentifier(NULL, NULL, 'id13')), 1);
// Test remote data.
$item_mapping->addRemoteData('test_data', 'test_value');
entity_save('tmgmt_remote', $item_mapping);
$item_mapping = entity_load_single('tmgmt_remote', $item_mapping->trid);
$this->assertEqual($item_mapping->getRemoteData('test_data'), 'test_value');
// Add mapping to the other job item as well.
$item2->addRemoteMapping($data_key, 'id21', array('remote_identifier_2' => 'id22', 'remote_identifier_3' => 'id23'));
// Test deleting.
// Delete item1.
entity_get_controller('tmgmt_job_item')->delete(array($item1->tjiid));
// Test if mapping for item1 has been removed as well.
$this->assertEqual(count($remote_mapping_controller->loadByLocalData(NULL, $item1->tjiid)), 0);
// We still should have mapping for item2.
$this->assertEqual(count($remote_mapping_controller->loadByLocalData(NULL, $item2->tjiid)), 1);
// Now delete the job and see if remaining mappings were removed as well.
entity_get_controller('tmgmt_job')->delete(array($job->tjid));
$this->assertEqual(count($remote_mapping_controller->loadByLocalData(NULL, $item2->tjiid)), 0);
}
/**
* Test crud operations of job items.
*/
function testJobItems() {
$job = $this->createJob();
// Add some test items.
$item1 = $job->addItem('test_source', 'type', 5);
$item2 = $job->addItem('test_source', 'test_with_long_label', 4);
// Test single load callback.
$item = tmgmt_job_item_load($item1->tjiid);
$this->assertEqual($item1->plugin, $item->plugin);
$this->assertEqual($item1->item_type, $item->item_type);
$this->assertEqual($item1->item_id, $item->item_id);
// Test multiple load callback.
$items = tmgmt_job_item_load_multiple(array($item1->tjiid, $item2->tjiid));
$this->assertEqual(2, count($items));
$this->assertEqual($item1->plugin, $items[$item1->tjiid]->plugin);
$this->assertEqual($item1->item_type, $items[$item1->tjiid]->item_type);
$this->assertEqual($item1->item_id, $items[$item1->tjiid]->item_id);
$this->assertEqual($item2->plugin, $items[$item2->tjiid]->plugin);
$this->assertEqual($item2->item_type, $items[$item2->tjiid]->item_type);
$this->assertEqual($item2->item_id, $items[$item2->tjiid]->item_id);
// Test the second item label length - it must not exceed the
// TMGMT_JOB_LABEL_MAX_LENGTH.
$this->assertTrue(TMGMT_JOB_LABEL_MAX_LENGTH >= strlen($items[$item2->tjiid]->label()));
}
/**
* Tests adding translated data and revision handling.
*/
function testAddingTranslatedData() {
$translator = $this->createTranslator();
$job = $this->createJob();
$job->translator = $translator->name;
$job->save();
// Add some test items.
$item1 = $job->addItem('test_source', 'test_with_long_label', 5);
// Test the job label - it must not exceed the TMGMT_JOB_LABEL_MAX_LENGTH.
$this->assertTrue(TMGMT_JOB_LABEL_MAX_LENGTH >= strlen($job->label()));
$key = array('dummy', 'deep_nesting');
$translation['dummy']['deep_nesting']['#text'] = 'translated 1';
$item1->addTranslatedData($translation);
$data = $item1->getData($key);
// Check job messages.
$messages = $job->getMessages();
$this->assertEqual(count($messages), 1);
$last_message = end($messages);
$this->assertEqual($last_message->message, 'The translation of !source to @language is finished and can now be <a href="!review_url">reviewed</a>.');
// Initial state - translation has been received for the first time.
$this->assertEqual($data['#translation']['#text'], 'translated 1');
$this->assertTrue(empty($data['#translation']['#text_revisions']));
$this->assertEqual($data['#translation']['#origin'], 'remote');
$this->assertEqual($data['#translation']['#timestamp'], REQUEST_TIME);
// Set status back to pending as if the data item was rejected.
$item1->updateData(array('dummy', 'deep_nesting'), array('#status' => TMGMT_DATA_ITEM_STATE_PENDING));
// Add same translation text.
$translation['dummy']['deep_nesting']['#text'] = 'translated 1';
$item1->addTranslatedData($translation);
$data = $item1->getData($key);
// Check if the status has been updated back to translated.
$this->assertEqual($data['#status'], TMGMT_DATA_ITEM_STATE_TRANSLATED);
// Add translation, however locally customized.
$translation['dummy']['deep_nesting']['#text'] = 'translated 2';
$translation['dummy']['deep_nesting']['#origin'] = 'local';
$translation['dummy']['deep_nesting']['#timestamp'] = REQUEST_TIME - 5;
$item1->addTranslatedData($translation);
$data = $item1->getData($key);
// The translation text is updated.
$this->assertEqual($data['#translation']['#text'], 'translated 2');
$this->assertEqual($data['#translation']['#timestamp'], REQUEST_TIME - 5);
// Previous translation is among text_revisions.
$this->assertEqual($data['#translation']['#text_revisions'][0]['#text'], 'translated 1');
$this->assertEqual($data['#translation']['#text_revisions'][0]['#origin'], 'remote');
$this->assertEqual($data['#translation']['#text_revisions'][0]['#timestamp'], REQUEST_TIME);
// Current translation origin is local.
$this->assertEqual($data['#translation']['#origin'], 'local');
// Check job messages.
$messages = $job->getMessages();
$this->assertEqual(count($messages), 1);
// Add translation - not local.
$translation['dummy']['deep_nesting']['#text'] = 'translated 3';
unset($translation['dummy']['deep_nesting']['#origin']);
unset($translation['dummy']['deep_nesting']['#timestamp']);
$item1->addTranslatedData($translation);
$data = $item1->getData($key);
// The translation text is NOT updated.
$this->assertEqual($data['#translation']['#text'], 'translated 2');
$this->assertEqual($data['#translation']['#timestamp'], REQUEST_TIME - 5);
// Received translation is the latest revision.
$last_revision = end($data['#translation']['#text_revisions']);
$this->assertEqual($last_revision['#text'], 'translated 3');
$this->assertEqual($last_revision['#origin'], 'remote');
$this->assertEqual($last_revision['#timestamp'], REQUEST_TIME);
// Current translation origin is local.
$this->assertEqual($data['#translation']['#origin'], 'local');
// Check job messages.
$messages = $job->getMessages();
$this->assertEqual(count($messages), 2);
$last_message = end($messages);
$this->assertEqual($last_message->message, 'Translation for customized @key received. Revert your changes if you wish to use it.');
// Revert to previous revision which is the latest received translation.
$item1->dataItemRevert($key);
$data = $item1->getData($key);
// The translation text is updated.
$this->assertEqual($data['#translation']['#text'], 'translated 3');
$this->assertEqual($data['#translation']['#origin'], 'remote');
$this->assertEqual($data['#translation']['#timestamp'], REQUEST_TIME);
// Latest revision is now the formerly added local translation.
$last_revision = end($data['#translation']['#text_revisions']);
$this->assertTrue($last_revision['#text'], 'translated 2');
$this->assertTrue($last_revision['#origin'], 'remote');
$this->assertEqual($last_revision['#timestamp'], REQUEST_TIME - 5);
// Check job messages.
$messages = $job->getMessages();
$this->assertEqual(count($messages), 3);
$last_message = end($messages);
$this->assertEqual($last_message->message, 'Translation for @key reverted to the latest version.');
// There should be three revisions now.
$this->assertEqual(count($data['#translation']['#text_revisions']), 3);
// Attempt to update the translation with the same text, this should not
// lead to a new revision.
$translation['dummy']['deep_nesting']['#text'] = 'translated 3';
//unset($translation['dummy']['deep_nesting']['#origin']);
//unset($translation['dummy']['deep_nesting']['#timestamp']);
$item1->addTranslatedData($translation);
$data = $item1->getData($key);
$this->assertEqual(count($data['#translation']['#text_revisions']), 3);
// Mark the translation as reviewed, a new translation should not update the
// existing one but create a new translation.
$item1->updateData($key, array('#status' => TMGMT_DATA_ITEM_STATE_REVIEWED));
$translation['dummy']['deep_nesting']['#text'] = 'translated 4';
$item1->addTranslatedData($translation);
$data = $item1->getData($key);
// The translation text is NOT updated.
$this->assertEqual($data['#translation']['#text'], 'translated 3');
// Received translation is the latest revision.
$this->assertEqual(count($data['#translation']['#text_revisions']), 4);
$last_revision = end($data['#translation']['#text_revisions']);
$this->assertEqual($last_revision['#text'], 'translated 4');
$this->assertEqual($last_revision['#origin'], 'remote');
$this->assertEqual($last_revision['#timestamp'], REQUEST_TIME);
// Check job messages.
$messages = $job->getMessages();
$this->assertEqual(count($messages), 4);
$last_message = end($messages);
$this->assertEqual($last_message->message, 'Translation for already reviewed @key received and stored as a new revision. Revert to it if you wish to use it.');
}
/**
* Test the calculations of the counters.
*/
function testJobItemsCounters() {
$job = $this->createJob();
// Some test data items.
$data1 = array(
'#text' => 'The text to be translated.',
);
$data2 = array(
'#text' => 'The text to be translated.',
'#translation' => '',
);
$data3 = array(
'#text' => 'The text to be translated.',
'#translation' => 'The translated data. Set by the translator plugin.',
);
$data4 = array(
'#text' => 'Another, longer text to be translated.',
'#translation' => 'The translated data. Set by the translator plugin.',
'#status' => TMGMT_DATA_ITEM_STATE_REVIEWED,
);
$data5 = array(
'#label' => 'label',
'data1' => $data1,
'data4' => $data4,
);
// No data items.
$this->assertEqual(0, $job->getCountPending());
$this->assertEqual(0, $job->getCountTranslated());
$this->assertEqual(0, $job->getCountReviewed());
$this->assertEqual(0, $job->getCountAccepted());
$this->assertEqual(0, $job->getWordCount());
// Add a test items.
$job_item1 = tmgmt_job_item_create('plugin', 'type', 4, array('tjid' => $job->tjid));
$job_item1->save();
// No pending, translated and confirmed data items.
$job = entity_load_single('tmgmt_job', $job->tjid);
$job_item1 = entity_load_single('tmgmt_job_item', $job_item1->tjiid);
drupal_static_reset('tmgmt_job_statistics_load');
$this->assertEqual(0, $job_item1->getCountPending());
$this->assertEqual(0, $job_item1->getCountTranslated());
$this->assertEqual(0, $job_item1->getCountReviewed());
$this->assertEqual(0, $job_item1->getCountAccepted());
$this->assertEqual(0, $job->getCountPending());
$this->assertEqual(0, $job->getCountTranslated());
$this->assertEqual(0, $job->getCountReviewed());
$this->assertEqual(0, $job->getCountAccepted());
// Add an untranslated data item.
$job_item1->data['data_item1'] = $data1;
$job_item1->save();
// One pending data items.
$job = entity_load_single('tmgmt_job', $job->tjid);
$job_item1 = entity_load_single('tmgmt_job_item', $job_item1->tjiid);
drupal_static_reset('tmgmt_job_statistics_load');
$this->assertEqual(1, $job_item1->getCountPending());
$this->assertEqual(0, $job_item1->getCountTranslated());
$this->assertEqual(0, $job_item1->getCountReviewed());
$this->assertEqual(5, $job_item1->getWordCount());
$this->assertEqual(1, $job->getCountPending());
$this->assertEqual(0, $job->getCountReviewed());
$this->assertEqual(0, $job->getCountTranslated());
$this->assertEqual(5, $job->getWordCount());
// Add another untranslated data item.
// Test with an empty translation set.
$job_item1->data['data_item1'] = $data2;
$job_item1->save();
// One pending data items.
$job = entity_load_single('tmgmt_job', $job->tjid);
$job_item1 = entity_load_single('tmgmt_job_item', $job_item1->tjiid);
drupal_static_reset('tmgmt_job_statistics_load');
$this->assertEqual(1, $job_item1->getCountPending());
$this->assertEqual(0, $job_item1->getCountTranslated());
$this->assertEqual(0, $job_item1->getCountReviewed());
$this->assertEqual(5, $job_item1->getWordCount());
$this->assertEqual(1, $job->getCountPending());
$this->assertEqual(0, $job->getCountTranslated());
$this->assertEqual(0, $job->getCountReviewed());
$this->assertEqual(5, $job->getWordCount());
// Add a translated data item.
$job_item1->data['data_item1'] = $data3;
$job_item1->save();
// One translated data items.
drupal_static_reset('tmgmt_job_statistics_load');
$this->assertEqual(0, $job_item1->getCountPending());
$this->assertEqual(1, $job_item1->getCountTranslated());
$this->assertEqual(0, $job_item1->getCountReviewed());
$this->assertEqual(0, $job->getCountPending());
$this->assertEqual(0, $job->getCountReviewed());
$this->assertEqual(1, $job->getCountTranslated());
// Add a confirmed data item.
$job_item1->data['data_item1'] = $data4;
$job_item1->save();
// One reviewed data item.
drupal_static_reset('tmgmt_job_statistics_load');
$this->assertEqual(1, $job_item1->getCountReviewed());
$this->assertEqual(1, $job->getCountReviewed());
// Add a translated and an untranslated and a confirmed data item
$job = entity_load_single('tmgmt_job', $job->tjid);
$job_item1 = entity_load_single('tmgmt_job_item', $job_item1->tjiid);
$job_item1->data['data_item1'] = $data1;
$job_item1->data['data_item2'] = $data3;
$job_item1->data['data_item3'] = $data4;
$job_item1->save();
// One pending and translated data items each.
drupal_static_reset('tmgmt_job_statistics_load');
$this->assertEqual(1, $job->getCountPending());
$this->assertEqual(1, $job->getCountTranslated());
$this->assertEqual(1, $job->getCountReviewed());
$this->assertEqual(16, $job->getWordCount());
// Add nested data items.
$job_item1->data['data_item1'] = $data5;
$job_item1->save();
// One pending data items.
$job = entity_load_single('tmgmt_job', $job->tjid);
$job_item1 = entity_load_single('tmgmt_job_item', $job_item1->tjiid);
$this->assertEqual('label', $job_item1->data['data_item1']['#label']);
$this->assertEqual(3, count($job_item1->data['data_item1']));
// Add a greater number of data items
for ($index = 1; $index <= 3; $index++) {
$job_item1->data['data_item' . $index] = $data1;
}
for ($index = 4; $index <= 10; $index++) {
$job_item1->data['data_item' . $index] = $data3;
}
for ($index = 11; $index <= 15; $index++) {
$job_item1->data['data_item' . $index] = $data4;
}
$job_item1->save();
// 3 pending and 7 translated data items each.
$job = entity_load_single('tmgmt_job', $job->tjid);
drupal_static_reset('tmgmt_job_statistics_load');
$this->assertEqual(3, $job->getCountPending());
$this->assertEqual(7, $job->getCountTranslated());
$this->assertEqual(5, $job->getCountReviewed());
// Add several job items
$job_item2 = tmgmt_job_item_create('plugin', 'type', 5, array('tjid' => $job->tjid));
for ($index = 1; $index <= 4; $index++) {
$job_item2->data['data_item' . $index] = $data1;
}
for ($index = 5; $index <= 12; $index++) {
$job_item2->data['data_item' . $index] = $data3;
}
for ($index = 13; $index <= 16; $index++) {
$job_item2->data['data_item' . $index] = $data4;
}
$job_item2->save();
// 3 pending and 7 translated data items each.
$job = entity_load_single('tmgmt_job', $job->tjid);
drupal_static_reset('tmgmt_job_statistics_load');
$this->assertEqual(7, $job->getCountPending());
$this->assertEqual(15, $job->getCountTranslated());
$this->assertEqual(9, $job->getCountReviewed());
// Accept the job items.
foreach ($job->getItems() as $item) {
// Set the state directly to avoid triggering translator and source
// controllers that do not exist.
$item->setState(TMGMT_JOB_ITEM_STATE_ACCEPTED);
$item->save();
}
drupal_static_reset('tmgmt_job_statistics_load');
$this->assertEqual(0, $job->getCountPending());
$this->assertEqual(0, $job->getCountTranslated());
$this->assertEqual(0, $job->getCountReviewed());
$this->assertEqual(31, $job->getCountAccepted());
}
}

View File

@@ -0,0 +1,123 @@
<?php
/*
* @file
* Contains tests for Translation management
*/
/**
* Test the helper functions in tmgmt.module.
*/
class TMGMTHelperTestCase extends TMGMTBaseTestCase {
static function getInfo() {
return array(
'name' => 'Helper functions Test case',
'description' => 'Helper functions for other modules',
'group' => 'Translation Management',
);
}
/**
* Tests tmgmt_job_match_item()
*
* @see tmgmt_job_match_item
*/
function testTMGTJobMatchItem() {
$this->loginAsAdmin();
$this->setEnvironment('fr');
$this->setEnvironment('es');
// Add a job from en to fr and en to sp.
$job_en_fr = $this->createJob('en', 'fr');
$job_en_sp = $this->createJob('en', 'es');
// Add a job which has existing source-target combinations.
$this->assertEqual($job_en_fr->tjid, tmgmt_job_match_item('en', 'fr')->tjid);
$this->assertEqual($job_en_sp->tjid, tmgmt_job_match_item('en', 'es')->tjid);
// Add a job which has no existing source-target combination.
$this->assertTrue(tmgmt_job_match_item('fr', 'es'));
}
/**
* Tests the tmgmt_data_item_label() function.
*
* @todo: Move into a unit test case once available.
*/
function testDataIemLabel() {
$no_label = array(
'#text' => 'No label',
);
$this->assertEqual(tmgmt_data_item_label($no_label), 'No label');
$this->assertEqual(tmgmt_data_item_label($no_label, 6), 'No ...');
$label = array(
'#parent_label' => array(),
'#label' => 'A label',
);
$this->assertEqual(tmgmt_data_item_label($label), 'A label');
$this->assertEqual(tmgmt_data_item_label($label, 6), 'A l...');
$parent_label = array(
'#parent_label' => array('Parent label', 'Sub label'),
'#label' => 'A label',
);
$this->assertEqual(tmgmt_data_item_label($parent_label), 'Parent label > Sub label');
$this->assertEqual(tmgmt_data_item_label($parent_label, 18), 'Pare... > Sub ...');
$nested = array(
'#parent_label' => array('Parent label', 'Sub label', 'Sub-sub label'),
'#label' => 'A label',
);
$this->assertEqual(tmgmt_data_item_label($nested), 'Parent label > Sub label > Sub-sub label');
$this->assertEqual(tmgmt_data_item_label($nested, 28), 'Pare... > Sub ... > Sub-...');
$long_label = array(
'#parent_label' => array('Loooooooooooong label', 'Short'),
'#label' => 'A label',
);
$this->assertEqual(tmgmt_data_item_label($long_label), 'Loooooooooooong label > Short');
$this->assertEqual(tmgmt_data_item_label($long_label, 30), 'Loooooooooooong label > Short');
$node_example = array(
'#parent_label' => array('This is a very loooong title, so looong', 'Body', 'Delta #0', 'Body'),
'#label' => 'A label',
);
$this->assertEqual(tmgmt_data_item_label($node_example), 'This is a very loooong title, so looong > Body > Delta #0 > Body');
$this->assertEqual(tmgmt_data_item_label($node_example, 56), 'This is a very loooong title... > Body > Delta #0 > Body');
}
function testWordCount() {
$unit_tests = array(
'empty' => array(
'text' => '',
'count' => 0,
),
'latin' => array(
'text' => 'Drupal is the best!',
'count' => 4,
),
'non-latin' => array(
'text' => 'Друпал лучший!',
'count' => 2,
),
'complex punctuation' => array(
'text' => '<[({-!ReAd@*;: ,?+MoRe...})]>\\|/',
'count' => 2,
'exclude_tags' => FALSE,
),
'repeat' => array(
'text' => 'repeat repeat',
'count' => 2,
),
'strip tags' => array(
'text' => '<a href="http://example.com">link text</a> plain text <div class="some-css-class"></div>',
'count' => 4,
),
);
foreach ($unit_tests as $id => $test_data) {
// Set the exclude_tags flag. In case not provided the TRUE is default.
$test_data += array('exclude_tags' => TRUE);
if (variable_get('tmgmt_word_count_exclude_tags', TRUE) != $test_data['exclude_tags']) {
variable_set('tmgmt_word_count_exclude_tags', $test_data['exclude_tags']);
}
$this->assertEqual($real_count = tmgmt_word_count($test_data['text']), $desirable_count = $test_data['count'], t('!test_id: Real count (=!real_count) should be equal to desirable (=!desirable_count)', array('!test_id' => $id, '!real_count' => $real_count, '!desirable_count' => $desirable_count)));
}
}
}

View File

@@ -0,0 +1,230 @@
<?php
/*
* @file
* Contains tests for Translation management
*/
/**
* Tests interaction between core and the plugins.
*/
class TMGMTPluginsTestCase extends TMGMTBaseTestCase {
static function getInfo() {
return array(
'name' => 'Plugin tests',
'description' => 'Verifies basic functionality of source and translator plugins',
'group' => 'Translation Management',
);
}
function createJob($source = 'en', $target = 'de', $uid = 1) {
$job = parent::createJob();
for ($i = 1; $i < 3; $i++) {
if ($i == 3) {
// Explicitly define the data for the third item.
$data['data'] = array(
'dummy' => array(
'deep_nesting' => array(
'#text' => 'Stored data',
),
),
);
$job->addItem('test_source', 'test', $i, array($data));
}
$job->addItem('test_source', 'test', $i);
}
// Manually specify the translator for now.
$job->translator = $this->default_translator->name;
return $job;
}
function testBasicWorkflow() {
// Submit a translation job.
$submit_job = $this->createJob();
$submit_job->settings = array('action' => 'submit');
$submit_job->requestTranslation();
$submit_job = tmgmt_job_load($submit_job->tjid);
$this->assertTrue($submit_job->isActive());
$messages = $submit_job->getMessages();
$last_message = end($messages);
$this->assertEqual('Test submit.', $last_message->message);
// Translate a job.
$translate_job = $this->createJob();
$translate_job->settings = array('action' => 'translate');
$translate_job->requestTranslation();
$translate_job = tmgmt_job_load($translate_job->tjid);
foreach ($translate_job->getItems() as $job_item) {
$this->assertTrue($job_item->isNeedsReview());
}
$messages = $translate_job->getMessages();
// array_values() results in numeric keys, which is necessary for list.
list($debug, $translated, $needs_review) = array_values($messages);
$this->assertEqual('Test translator called.', $debug->message);
$this->assertEqual('debug', $debug->type);
$this->assertEqual('Test translation created.', $translated->message);
$this->assertEqual('status', $translated->type);
// The third message is specific to a job item and has different state
// constants.
$this->assertEqual('The translation of !source to @language is finished and can now be <a href="!review_url">reviewed</a>.', $needs_review->message);
$this->assertEqual('status', $needs_review->type);
$i = 1;
foreach ($translate_job->getItems() as $item) {
// Check the translated text.
if ($i != 3) {
$expected_text = 'de_Text for job item with type ' . $item->item_type . ' and id ' . $item->item_id . '.';
}
else {
// The third item has an explicitly stored data value.
$expected_text = 'de_Stored data';
}
$item_data = $item->getData();
$this->assertEqual($expected_text, $item_data['dummy']['deep_nesting']['#translation']['#text']);
$i++;
}
foreach ($translate_job->getItems() as $job_item) {
$job_item->acceptTranslation();
}
// @todo Accepting does not result in messages on the job anymore.
// Update once there are job item messages.
/*
$messages = $translate_job->getMessages();
$last_message = end($messages);
$this->assertEqual('Job accepted', $last_message->message);
$this->assertEqual('status', $last_message->type);*/
// Check if the translations have been "saved".
foreach ($translate_job->getItems() as $item) {
$this->assertTrue(variable_get('tmgmt_test_saved_translation_' . $item->item_type . '_' . $item->item_id, FALSE));
}
// A rejected job.
$reject_job = $this->createJob();
$reject_job->settings = array('action' => 'reject');
$reject_job->requestTranslation();
// Still rejected.
$this->assertTrue($reject_job->isRejected());
$messages = $reject_job->getMessages();
$last_message = end($messages);
$this->assertEqual('This is not supported.', $last_message->message);
$this->assertEqual('error', $last_message->type);
// A failing job.
$failing_job = $this->createJob();
$failing_job->settings = array('action' => 'fail');
$failing_job->requestTranslation();
// Still new.
$this->assertTrue($failing_job->isUnprocessed());
$messages = $failing_job->getMessages();
$last_message = end($messages);
$this->assertEqual('Service not reachable.', $last_message->message);
$this->assertEqual('error', $last_message->type);
}
/**
* Tests remote languages mappings support in the tmgmt core.
*/
function testRemoteLanguagesMappings() {
$this->loginAsAdmin();
$this->setEnvironment('de');
$controller = $this->default_translator->getController();
$mappings = $controller->getRemoteLanguagesMappings($this->default_translator);
$this->assertEqual($mappings, array(
'en' => 'en-us',
'de' => 'de-ch',
));
$this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'en'), 'en-us');
$this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'de'), 'de-ch');
$this->assertEqual($controller->mapToLocalLanguage($this->default_translator, 'en-us'), 'en');
$this->assertEqual($controller->mapToLocalLanguage($this->default_translator, 'de-ch'), 'de');
$this->default_translator->settings['remote_languages_mappings']['de'] = 'de-de';
$this->default_translator->settings['remote_languages_mappings']['en'] = 'en-uk';
$this->default_translator->save();
$this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'en'), 'en-uk');
$this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'de'), 'de-de');
// Test the fallback.
$info = &drupal_static('_tmgmt_plugin_info');
$info['translator']['test_translator']['map remote languages'] = FALSE;
$this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'en'), 'en');
$this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'de'), 'de');
}
/**
* Tests escaping and unescaping text.
*/
function testEscaping() {
$controller = $this->default_translator->getController();
$tests = array(
array(
'item' => array('#text' => 'no escaping'),
'expected' => 'no escaping',
),
array(
'item' => array(
'#text' => 'single placeholder',
'#escape' => array(
7 => array('string' => 'placeholder'),
),
),
'expected' => 'single [[[placeholder]]]',
),
array(
'item' => array(
'#text' => 'two placeholder, the second placeholder',
'#escape' => array(
4 => array('string' => 'placeholder'),
28 => array('string' => 'placeholder'),
),
),
'expected' => 'two [[[placeholder]]], the second [[[placeholder]]]',
),
array(
'item' => array(
'#text' => 'something, something else',
'#escape' => array(
0 => array('string' => 'something'),
21 => array('string' => 'else'),
),
),
'expected' => '[[[something]]], something [[[else]]]',
),
array(
'item' => array(
'#text' => 'something, something else',
'#escape' => array(
21 => array('string' => 'else'),
11 => array('string' => 'something'),
),
),
'expected' => 'something, [[[something]]] [[[else]]]',
),
);
foreach ($tests as $test) {
$escaped = $controller->escapeText($test['item']);
// Assert that the string was escaped as expected.
$this->assertEqual($escaped, $test['expected']);
// Assert that the string is the same as the original when unescaped.
$this->assertEqual($controller->unescapeText($escaped), $test['item']['#text']);
}
}
}

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