Add Babel (Request #7853).
authorJan Schneider <jan@horde.org>
Mon, 19 Jan 2009 09:44:39 +0000 (10:44 +0100)
committerJan Schneider <jan@horde.org>
Mon, 19 Jan 2009 09:44:39 +0000 (10:44 +0100)
55 files changed:
babel/COPYING [new file with mode: 0644]
babel/README [new file with mode: 0644]
babel/commit.php [new file with mode: 0644]
babel/config/hooks.php.dist [new file with mode: 0644]
babel/docs/CHANGES [new file with mode: 0644]
babel/docs/CREDITS [new file with mode: 0644]
babel/docs/TODO [new file with mode: 0644]
babel/download.php [new file with mode: 0644]
babel/edit.php [new file with mode: 0644]
babel/extract.php [new file with mode: 0644]
babel/index.php [new file with mode: 0644]
babel/js/effects.js [new file with mode: 0644]
babel/js/prototype.js [new file with mode: 0644]
babel/js/redbox.js [new file with mode: 0644]
babel/lib/Display.php [new file with mode: 0644]
babel/lib/Gettext.php [new file with mode: 0644]
babel/lib/Gettext/MO.php [new file with mode: 0644]
babel/lib/Gettext/PO.php [new file with mode: 0644]
babel/lib/Translate.php [new file with mode: 0644]
babel/lib/Translate_Help.php [new file with mode: 0644]
babel/lib/Translation.php [new file with mode: 0644]
babel/lib/api.php [new file with mode: 0644]
babel/lib/base.php [new file with mode: 0644]
babel/make.php [new file with mode: 0644]
babel/po/fr_FR.po [new file with mode: 0644]
babel/po/translation.pot [new file with mode: 0644]
babel/reset.php [new file with mode: 0644]
babel/scripts/translate.php [new file with mode: 0755]
babel/stats.php [new file with mode: 0644]
babel/templates/common-header.inc [new file with mode: 0644]
babel/templates/index.php [new file with mode: 0644]
babel/templates/layout.html [new file with mode: 0644]
babel/themes/graphics/az.png [new file with mode: 0644]
babel/themes/graphics/checked.gif [new file with mode: 0644]
babel/themes/graphics/config.png [new file with mode: 0644]
babel/themes/graphics/delete.png [new file with mode: 0644]
babel/themes/graphics/down.png [new file with mode: 0644]
babel/themes/graphics/edit.png [new file with mode: 0644]
babel/themes/graphics/extract.png [new file with mode: 0644]
babel/themes/graphics/list.png [new file with mode: 0644]
babel/themes/graphics/locked.png [new file with mode: 0644]
babel/themes/graphics/make.png [new file with mode: 0644]
babel/themes/graphics/redbox_spinner.gif [new file with mode: 0644]
babel/themes/graphics/sample.png [new file with mode: 0644]
babel/themes/graphics/skeleton.png [new file with mode: 0644]
babel/themes/graphics/translation.png [new file with mode: 0644]
babel/themes/graphics/unchecked.gif [new file with mode: 0644]
babel/themes/graphics/up.png [new file with mode: 0644]
babel/themes/graphics/upload.png [new file with mode: 0644]
babel/themes/graphics/view.png [new file with mode: 0644]
babel/themes/graphics/za.png [new file with mode: 0644]
babel/themes/screen.css [new file with mode: 0644]
babel/upload.php [new file with mode: 0644]
babel/view.php [new file with mode: 0644]
babel/viewsource.php [new file with mode: 0644]

diff --git a/babel/COPYING b/babel/COPYING
new file mode 100644 (file)
index 0000000..a6b6756
--- /dev/null
@@ -0,0 +1,280 @@
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright 1989, 1991 Free Software Foundation, Inc.
+     59 Temple Place, Suite 330, Boston, MA  02111-1307  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 Library 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.
+\f
+                   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.)
+\f
+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.
+\f
+  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.
+\f
+  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
diff --git a/babel/README b/babel/README
new file mode 100644 (file)
index 0000000..e05f7f6
--- /dev/null
@@ -0,0 +1,86 @@
+What is Babel?
+==============
+
+:Contact:       joel@scopserv.com
+
+.. contents:: Contents
+.. section-numbering::
+
+Babel is a web interface to viewing and editing PO (gettext) files for all
+modules written in PHP and utilizing the `Horde Application Framework`_.
+
+This software is OSI Certified Open Source Software.  OSI Certified is a
+certification mark of the `Open Source Initiative`_.
+
+.. _`Horde Application Framework`: http://www.horde.org/horde/
+.. _`Open Source Initiative`: http://www.opensource.org/
+
+
+Obtaining Babel
+---------------
+
+Further information on Babel and the latest version can be obtained at
+
+  http://.../
+
+
+Documentation
+-------------
+
+The following documentation is available in the Babel distribution:
+
+:README_:          This file
+:COPYING_:         Copyright and license information
+:`docs/BUGS`_:     Known bugs
+:`docs/CHANGES`_:  Changes by release
+:`docs/CREDITS`_:  Project developers
+:`docs/INSTALL`_:  Installation instructions and notes
+:`docs/TODO`_:     Development TODO list
+
+
+Installation
+------------
+
+Instructions for installing Babel can be found in the file INSTALL_ in the
+``docs/`` directory of the Babel distribution.
+
+
+Assistance
+----------
+
+If you encounter problems with Babel, help is available!
+
+The Horde Frequently Asked Questions List (FAQ), available on the Web at
+
+  http://www.horde.org/faq/
+
+The Horde Project runs a number of mailing lists, for individual applications
+and for issues relating to the project as a whole.  Information, archives, and
+subscription information can be found at
+
+  http://www.horde.org/mail/
+
+Lastly, Horde developers, contributors and users also make occasional
+appearances on IRC, on the channel #horde on the freenode Network
+(irc.freenode.net).
+
+
+Licensing
+---------
+
+For licensing and copyright information, please see the file COPYING_ in the
+Babel distribution.
+
+Thanks,
+
+The Babel team
+
+
+.. _README: ?f=README.html
+.. _COPYING: http://www.horde.org/licenses/gpl.php
+.. _docs/BUGS: ?f=BUGS.html
+.. _docs/CHANGES: ?f=CHANGES.html
+.. _docs/CREDITS: ?f=CREDITS.html
+.. _INSTALL:
+.. _docs/INSTALL: ?f=INSTALL.html
+.. _docs/TODO: ?f=TODO.html
diff --git a/babel/commit.php b/babel/commit.php
new file mode 100644 (file)
index 0000000..6bd098a
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+/**
+ * Copyright 2000-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author  Joel Vandal <joel@scopserv.com>
+ * @package Babel
+ */
+
+@define('BABEL_BASE', dirname(__FILE__)) ;
+require_once BABEL_BASE . '/lib/base.php';
+
+// Define if we use Horde CVS or Custom Commit
+$custom_commit = true;
+
+/* Render the page. */
+require BABEL_TEMPLATES . '/common-header.inc';
+echo $template->fetch(BABEL_TEMPLATES . '/layout.html');
+
+Translate_Display::header(_("Horde translation generator"));
+
+/* Do sanity check */
+Translate::sanity_check();
+
+/* Searching applications */
+Translate::check_binaries();
+
+Translate_Display::info(sprintf('Searching Horde applications in %s', realpath(HORDE_BASE)));
+$dirs = Translate::search_applications();
+
+$apps = Translate::strip_horde($dirs);
+$apps[0] = 'horde';
+Translate_Display::info(_("Found applications:"));
+Translate_Display::info(wordwrap(implode(', ', $apps)), false);
+Translate_Display::info();
+
+// Check if we must execute Custom commit or Horde CVS Commit (Developer)
+if ($custom_commit) {
+    Translate_Display::header(_("Commit PO files ..."));
+    foreach($dirs as $d => $dir) {
+       $dir = realpath($dir);
+       $po  = $dir . '/po/' . $lang . '.po';
+       
+       if (@file_exists($po)) {
+           Translate_Display::info(_("Commit") . " $po ($lang)");
+           Translation::callHook('commit', array($po, $lang));
+       }
+       
+    }
+} else {
+    Translate::commit();
+}
+
+
+Translate_Display::info();
+
+require $registry->get('templates', 'horde') . '/common-footer.inc';
diff --git a/babel/config/hooks.php.dist b/babel/config/hooks.php.dist
new file mode 100644 (file)
index 0000000..ae9ac5c
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+
+if (!function_exists('_translation_hook_commit')) {
+    function _translation_hook_commit($info)
+    {
+       $po   = $info[0];
+       $lang = $info[1];
+       $cmd = "svn commit $po -m \"- Update $lang translation.\"";
+       Translate_Display::warning($cmd);
+    }
+}
+
+if (!function_exists('_translation_hook_reset')) {
+    function _translation_hook_reset($po)
+    {
+
+       $cmd = "rm -rf $po";
+       exec("sudo $cmd");
+       
+       $cmd = "svn update $po";
+       exec("sudo $cmd");
+       
+       if (!@file_exists($po)) {
+           Translate_Display::warning(_("File doesn't exist ... ") . $po);
+           Translate_Display::info();
+       }
+    }
+}
diff --git a/babel/docs/CHANGES b/babel/docs/CHANGES
new file mode 100644 (file)
index 0000000..c417e9f
--- /dev/null
@@ -0,0 +1,5 @@
+----------
+v0.1-dev
+----------
+
+[jvandal] ...
diff --git a/babel/docs/CREDITS b/babel/docs/CREDITS
new file mode 100644 (file)
index 0000000..7177797
--- /dev/null
@@ -0,0 +1,15 @@
+========================
+ Babel Development Team
+========================
+
+Joel Vandal <joel@scopserv.com>
+
+Thanks to Michael Wallner <mike@php.net> for File_Gettext PEAR library.
+   
+
+Localization
+============
+
+=====================   ======================================================
+French                  Joel Vandal <joel@scopserv.com>
+=====================   ======================================================
diff --git a/babel/docs/TODO b/babel/docs/TODO
new file mode 100644 (file)
index 0000000..44a1742
--- /dev/null
@@ -0,0 +1,8 @@
+=============================
+ Babel Development TODO List
+=============================
+
+:Contact:       joel@scopsev.com
+
+- Add support for Obsolete strings.
+
diff --git a/babel/download.php b/babel/download.php
new file mode 100644 (file)
index 0000000..fa20f86
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+/**
+ * Copyright 2000-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author  Joel Vandal <joel@scopserv.com>
+ * @package Babel
+ */
+
+$no_compress = true;
+
+$title = _("Download File");
+
+@define('BABEL_BASE', dirname(__FILE__));
+require_once BABEL_BASE . '/lib/base.php';
+
+$files = array();
+$dirs = Translate::search_applications();
+foreach($dirs as $d => $dir) {
+    $dir = realpath($dir);
+    
+    $app = str_replace(realpath(HORDE_BASE), '', $dir);
+    $app = str_replace('/', '', $app);
+    if (empty($app)) {
+       $app = 'horde';
+    }
+
+    $po  = $dir . '/po/' . $lang . '.po';
+    if (@file_exists($po)) {
+       $files[$app] = $po;
+    }
+}
+
+$filename = "po-" . $lang . ".zip";
+@system("rm -rf /tmp/$filename");
+@system("rm -rf /tmp/translate");
+@mkdir("/tmp/translate");
+foreach($files as $app => $file) {
+    $cmd = "cp $file /tmp/translate/$app-" . basename($file);
+    @system($cmd);
+}
+$cmd = "zip -j /tmp/$filename /tmp/translate/*";
+@exec($cmd);
+
+$data = file_get_contents("/tmp/$filename");
+
+$browser->downloadHeaders($filename);
+echo $data;
diff --git a/babel/edit.php b/babel/edit.php
new file mode 100644 (file)
index 0000000..1dd567d
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+/**
+ * Copyright 2000-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author  Joel Vandal <joel@scopserv.com>
+ * @package Babel
+ */
+
+$meta_params = array(
+                    "Project-Id-Version" => @$_SESSION['translation']['language'],
+                    "Report-Msgid-Bugs-To" => "support@scopserv.com",
+                    "POT-Creation-Date" => "",
+                    "PO-Revision-Date" => "",
+                    "Last-Translator" => "",
+                    "Language-Team" => "",
+                    "MIME-Version" => "1.0",
+                    "Content-Type" => "text/plain; charset=utf-8",
+                    "Content-Transfer-Encoding" => "8bit",
+                    "Plural-Forms" => "nplurals=2; plural=(n > 1);");
+
+
+require_once dirname(__FILE__) . '/lib/base.php';
+require_once BABEL_BASE . '/lib/Gettext/PO.php';
+
+require_once 'Horde/Form.php';
+require_once 'Horde/Variables.php';
+require_once 'Horde/Form/Renderer.php';
+require_once 'Horde/Form/Action.php';
+
+require_once 'Horde/UI/Tabs.php';
+
+$app = Util::getFormData('module');
+
+$show = 'edit';
+$vars = &Variables::getDefaultVariables();
+
+if ($app) {
+    $napp = ($app == 'horde') ? '' : $app;
+    $pofile = HORDE_BASE . '/' . $napp . '/po/' . $_SESSION['translation']['language'] . '.po';
+    $po = &new File_Gettext_PO();
+    $po->load($pofile);
+}
+
+/* Set up the template fields. */
+$template->set('menu', Translation::getMenu('string'));
+$template->set('notify', Util::bufferOutput(array($notification, 'notify'), array('listeners' => 'status')));
+
+/* Create upload form */
+$form = &new Horde_Form($vars, _("Edit Translation"), $show);
+
+/* Validate form if submitted */
+if ($app && Util::getFormData('submitbutton') == _("Save")) {
+
+    if ($form->validate($vars, false)) {
+       $form->getInfo($vars, $form_values);
+       
+       foreach($meta_params as $k => $v) {
+           if ($val = Util::getFormData($k)) {
+               $po->meta[$k] = $val;
+           }
+       }
+       
+       $po->save($pofile);
+       
+       if (Util::getFormData('url') == 'view') {
+           $url = Horde::applicationUrl('view.php');
+           $url = Util::addParameter($url, array('module' => $app));
+           header('Location: ' . $url);
+           exit;
+       }
+    }
+}
+
+if (!$app) {
+    $form->setButtons(_("Edit"));
+    $form->addVariable(_("Module"), 'module', 'enum', true, false, null, array(Translation::listApps(), true));
+    $form->addVariable('', '', 'spacer', true);
+} else {
+
+    $form->setButtons(_("Save"));
+    $form->addHidden('', 'module', 'text', false);
+    $vars->set('module', $app);
+    
+    $form->addHidden('', 'url', 'text', false);
+    $vars->set('url', Util::getFormData('url'));
+    
+    foreach($meta_params as $k => $v) {
+       $form->addVariable($k, $k, 'text', false, false);
+       if (isset($po->meta[$k]) && !empty($po->meta[$k])) {
+           $vars->set($k, $po->meta[$k]);
+       } elseif (!empty($v)) {
+           $vars->set($k, $v);
+       }
+    }
+}
+
+/* Render the page. */
+require BABEL_TEMPLATES . '/common-header.inc';
+
+echo $template->fetch(BABEL_TEMPLATES . '/layout.html');
+
+$renderer_params = array();
+$renderer = &new Horde_Form_Renderer($renderer_params);
+$renderer->setAttrColumnWidth('20%');
+
+$form->renderActive($renderer, $vars, Horde::selfURL(), 'post');
+
+require $registry->get('templates', 'horde') . '/common-footer.inc';
diff --git a/babel/extract.php b/babel/extract.php
new file mode 100644 (file)
index 0000000..377b82d
--- /dev/null
@@ -0,0 +1,119 @@
+<?php
+/**
+ * Copyright 2000-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author  Joel Vandal <joel@scopserv.com>
+ * @package Babel
+ */
+
+set_time_limit(0);
+
+@define('BABEL_BASE', dirname(__FILE__)) ;
+require_once BABEL_BASE . '/lib/base.php';
+
+if ($app) {
+    Translation::RB_init();
+}
+
+/* Render the page. */
+require BABEL_TEMPLATES . '/common-header.inc';
+
+if ($app) {
+    Translation::RB_start(300);
+}
+
+echo $template->fetch(BABEL_TEMPLATES . '/layout.html');
+
+$vars = &Variables::getDefaultVariables();
+
+/* Create upload form */
+$form = &new Horde_Form($vars, _("Extract Translation"), 'extract');
+
+if (!$app) {
+    $form->setButtons(_("Extract"));
+    $form->addVariable(_("Module"), 'module', 'enum', true, false, null, array(Translation::listApps(true), true));
+    $form->addVariable('', '', 'spacer', true);
+    
+    $renderer_params = array();
+    $renderer = &new Horde_Form_Renderer($renderer_params);
+    $renderer->setAttrColumnWidth('20%');
+    
+    $form->renderActive($renderer, $vars, Horde::selfURL(), 'post');
+} else {
+    Translate_Display::header(_("Horde translation generator"));
+    
+    /* Sanity checks */
+    if (!extension_loaded('gettext')) {
+       Translate_Display::error(_("Gettext extension not found!"));
+       footer();
+    }
+    
+    Translate_Display::info(_("Loading libraries..."));
+    $libs_found = true;
+    
+    foreach (array('Console_Getopt' => 'Console/Getopt.php',
+                  'Console_Table'  => 'Console/Table.php',
+                  'File_Find'      => 'File/Find.php')
+            as $class => $file) {
+       @include_once $file;
+       if (class_exists($class)) {
+           // Translate_Display::info("$class ...", false);
+       } else {
+           Translate_Display::error(sprintf(_("%s not found."), $class));
+           $libs_found = false;
+       }
+    }
+    
+    if (!$libs_found) {
+       Translate_Display::info();
+       Translate_Display::info(_("Make sure that you have PEAR installed and in your include path."));
+       Translate_Display::info('include_path: ' . ini_get('include_path'));
+    }
+    Translate_Display::info();
+    
+    /* Searching applications */
+    Translate::check_binaries();
+    
+    Translate_Display::info(sprintf(_("Searching Horde applications in %s"), realpath(HORDE_BASE)));
+    $dirs = Translate::search_applications();
+    
+    if ($app == 'ALL') {
+       Translate_Display::info(_("Found directories:"), false);
+       Translate_Display::info(implode("\n", $dirs), false);
+    }
+    Translate_Display::info();
+    
+    $apps = Translate::strip_horde($dirs);
+    $apps[0] = 'horde';
+    if ($app == 'ALL') {
+       Translate_Display::info(_("Found applications:"));
+       Translate_Display::info(wordwrap(implode(', ', $apps)), false);
+       Translate_Display::info();
+    }
+    
+    global $module;
+    if ($app != 'ALL') {
+       $module = $app;
+    }
+    
+    Translate::init();
+    Translate::cleanup();
+    
+    Translate_Display::header(_("Generate Compendium ..."));
+    Translate::compendium();
+    Translate_Display::info();
+    
+    Translate::xtract();
+    Translate_Display::info();
+    Translate::merge();
+    
+    Translate_Display::info();
+    Translate_Display::header(_("Done!"));
+    
+    Translation::RB_close();
+}
+
+require $registry->get('templates', 'horde') . '/common-footer.inc';
diff --git a/babel/index.php b/babel/index.php
new file mode 100644 (file)
index 0000000..06f00c2
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+/**
+ * Copyright 2000-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author  Joel Vandal <joel@scopserv.com>
+ * @package Babel
+ */
+
+@define('BABEL_BASE', dirname(__FILE__)) ;
+require_once BABEL_BASE . '/lib/base.php';
+
+/* Render the page. */
+require BABEL_TEMPLATES . '/common-header.inc';
+echo $template->fetch(BABEL_TEMPLATES . '/layout.html');
+
+require BABEL_TEMPLATES . '/index.php';
+
+require $registry->get('templates', 'horde') . '/common-footer.inc';
diff --git a/babel/js/effects.js b/babel/js/effects.js
new file mode 100644 (file)
index 0000000..ff8cb9e
--- /dev/null
@@ -0,0 +1,65 @@
+
+String.prototype.parseColor=function(){var color='#';if(this.slice(0,4)=='rgb('){var cols=this.slice(4,this.length-1).split(',');var i=0;do{color+=parseInt(cols[i]).toColorPart()}while(++i<3);}else{if(this.slice(0,1)=='#'){if(this.length==4)for(var i=1;i<4;i++)color+=(this.charAt(i)+this.charAt(i)).toLowerCase();if(this.length==7)color=this.toLowerCase();}}
+return(color.length==7?color:(arguments[0]||this));}
+Element.collectTextNodes=function(element){return $A($(element).childNodes).collect(function(node){return(node.nodeType==3?node.nodeValue:(node.hasChildNodes()?Element.collectTextNodes(node):''));}).flatten().join('');}
+Element.collectTextNodesIgnoreClass=function(element,className){return $A($(element).childNodes).collect(function(node){return(node.nodeType==3?node.nodeValue:((node.hasChildNodes()&&!Element.hasClassName(node,className))?Element.collectTextNodes(node):''));}).flatten().join('');}
+Element.setStyle=function(element,style){element=$(element);for(k in style)element.style[k.camelize()]=style[k];}
+Element.setContentZoom=function(element,percent){Element.setStyle(element,{fontSize:(percent/100)+'em'});if(navigator.appVersion.indexOf('AppleWebKit')>0)window.scrollBy(0,0);}
+Element.getOpacity=function(element){var opacity;if(opacity=Element.getStyle(element,'opacity'))
+return parseFloat(opacity);if(opacity=(Element.getStyle(element,'filter')||'').match(/alpha\(opacity=(.*)\)/))
+if(opacity[1])return parseFloat(opacity[1])/100;return 1.0;}
+Element.setOpacity=function(element,value){element=$(element);if(value==1){Element.setStyle(element,{opacity:(/Gecko/.test(navigator.userAgent)&&!/Konqueror|Safari|KHTML/.test(navigator.userAgent))?0.999999:null});if(/MSIE/.test(navigator.userAgent))
+Element.setStyle(element,{filter:Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});}else{if(value<0.00001)value=0;Element.setStyle(element,{opacity:value});if(/MSIE/.test(navigator.userAgent))
+Element.setStyle(element,{filter:Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')+'alpha(opacity='+value*100+')'});}}
+Element.getInlineOpacity=function(element){return $(element).style.opacity||'';}
+Element.childrenWithClassName=function(element,className){return $A($(element).getElementsByTagName('*')).select(function(c){return Element.hasClassName(c,className)});}
+Array.prototype.call=function(){var args=arguments;this.each(function(f){f.apply(this,args)});}
+var Effect={tagifyText:function(element){var tagifyStyle='position:relative';if(/MSIE/.test(navigator.userAgent))tagifyStyle+=';zoom:1';element=$(element);$A(element.childNodes).each(function(child){if(child.nodeType==3){child.nodeValue.toArray().each(function(character){element.insertBefore(Builder.node('span',{style:tagifyStyle},character==' '?String.fromCharCode(160):character),child);});Element.remove(child);}});},multiple:function(element,effect){var elements;if(((typeof element=='object')||(typeof element=='function'))&&(element.length))
+elements=element;else
+elements=$(element).childNodes;var options=Object.extend({speed:0.1,delay:0.0},arguments[2]||{});var masterDelay=options.delay;$A(elements).each(function(element,index){new effect(element,Object.extend(options,{delay:index*options.speed+masterDelay}));});},PAIRS:{'slide':['SlideDown','SlideUp'],'blind':['BlindDown','BlindUp'],'appear':['Appear','Fade']},toggle:function(element,effect){element=$(element);effect=(effect||'appear').toLowerCase();var options=Object.extend({queue:{position:'end',scope:(element.id||'global')}},arguments[2]||{});Effect[Element.visible(element)?Effect.PAIRS[effect][1]:Effect.PAIRS[effect][0]](element,options);}};var Effect2=Effect;Effect.Transitions={}
+Effect.Transitions.linear=function(pos){return pos;}
+Effect.Transitions.sinoidal=function(pos){return(-Math.cos(pos*Math.PI)/2)+0.5;}
+Effect.Transitions.reverse=function(pos){return 1-pos;}
+Effect.Transitions.flicker=function(pos){return((-Math.cos(pos*Math.PI)/4)+0.75)+Math.random()/4;}
+Effect.Transitions.wobble=function(pos){return(-Math.cos(pos*Math.PI*(9*pos))/2)+0.5;}
+Effect.Transitions.pulse=function(pos){return(Math.floor(pos*10)%2==0?(pos*10-Math.floor(pos*10)):1-(pos*10-Math.floor(pos*10)));}
+Effect.Transitions.none=function(pos){return 0;}
+Effect.Transitions.full=function(pos){return 1;}
+Effect.ScopedQueue=Class.create();Object.extend(Object.extend(Effect.ScopedQueue.prototype,Enumerable),{initialize:function(){this.effects=[];this.interval=null;},_each:function(iterator){this.effects._each(iterator);},add:function(effect){var timestamp=new Date().getTime();var position=(typeof effect.options.queue=='string')?effect.options.queue:effect.options.queue.position;switch(position){case'front':this.effects.findAll(function(e){return e.state=='idle'}).each(function(e){e.startOn+=effect.finishOn;e.finishOn+=effect.finishOn;});break;case'end':timestamp=this.effects.pluck('finishOn').max()||timestamp;break;}
+effect.startOn+=timestamp;effect.finishOn+=timestamp;this.effects.push(effect);if(!this.interval)
+this.interval=setInterval(this.loop.bind(this),40);},remove:function(effect){this.effects=this.effects.reject(function(e){return e==effect});if(this.effects.length==0){clearInterval(this.interval);this.interval=null;}},loop:function(){var timePos=new Date().getTime();this.effects.invoke('loop',timePos);}});Effect.Queues={instances:$H(),get:function(queueName){if(typeof queueName!='string')return queueName;if(!this.instances[queueName])
+this.instances[queueName]=new Effect.ScopedQueue();return this.instances[queueName];}}
+Effect.Queue=Effect.Queues.get('global');Effect.DefaultOptions={transition:Effect.Transitions.sinoidal,duration:1.0,fps:25.0,sync:false,from:0.0,to:1.0,delay:0.0,queue:'parallel'}
+Effect.Base=function(){};Effect.Base.prototype={position:null,start:function(options){this.options=Object.extend(Object.extend({},Effect.DefaultOptions),options||{});this.currentFrame=0;this.state='idle';this.startOn=this.options.delay*1000;this.finishOn=this.startOn+(this.options.duration*1000);this.event('beforeStart');if(!this.options.sync)
+Effect.Queues.get(typeof this.options.queue=='string'?'global':this.options.queue.scope).add(this);},loop:function(timePos){if(timePos>=this.startOn){if(timePos>=this.finishOn){this.render(1.0);this.cancel();this.event('beforeFinish');if(this.finish)this.finish();this.event('afterFinish');return;}
+var pos=(timePos-this.startOn)/(this.finishOn-this.startOn);var frame=Math.round(pos*this.options.fps*this.options.duration);if(frame>this.currentFrame){this.render(pos);this.currentFrame=frame;}}},render:function(pos){if(this.state=='idle'){this.state='running';this.event('beforeSetup');if(this.setup)this.setup();this.event('afterSetup');}
+if(this.state=='running'){if(this.options.transition)pos=this.options.transition(pos);pos*=(this.options.to-this.options.from);pos+=this.options.from;this.position=pos;this.event('beforeUpdate');if(this.update)this.update(pos);this.event('afterUpdate');}},cancel:function(){if(!this.options.sync)
+Effect.Queues.get(typeof this.options.queue=='string'?'global':this.options.queue.scope).remove(this);this.state='finished';},event:function(eventName){if(this.options[eventName+'Internal'])this.options[eventName+'Internal'](this);if(this.options[eventName])this.options[eventName](this);},inspect:function(){return'#<Effect:'+$H(this).inspect()+',options:'+$H(this.options).inspect()+'>';}}
+Effect.Parallel=Class.create();Object.extend(Object.extend(Effect.Parallel.prototype,Effect.Base.prototype),{initialize:function(effects){this.effects=effects||[];this.start(arguments[1]);},update:function(position){this.effects.invoke('render',position);},finish:function(position){this.effects.each(function(effect){effect.render(1.0);effect.cancel();effect.event('beforeFinish');if(effect.finish)effect.finish(position);effect.event('afterFinish');});}});Effect.Opacity=Class.create();Object.extend(Object.extend(Effect.Opacity.prototype,Effect.Base.prototype),{initialize:function(element){this.element=$(element);if(/MSIE/.test(navigator.userAgent)&&(!this.element.hasLayout))
+Element.setStyle(this.element,{zoom:1});var options=Object.extend({from:Element.getOpacity(this.element)||0.0,to:1.0},arguments[1]||{});this.start(options);},update:function(position){Element.setOpacity(this.element,position);}});Effect.Move=Class.create();Object.extend(Object.extend(Effect.Move.prototype,Effect.Base.prototype),{initialize:function(element){this.element=$(element);var options=Object.extend({x:0,y:0,mode:'relative'},arguments[1]||{});this.start(options);},setup:function(){Element.makePositioned(this.element);this.originalLeft=parseFloat(Element.getStyle(this.element,'left')||'0');this.originalTop=parseFloat(Element.getStyle(this.element,'top')||'0');if(this.options.mode=='absolute'){this.options.x=this.options.x-this.originalLeft;this.options.y=this.options.y-this.originalTop;}},update:function(position){Element.setStyle(this.element,{left:this.options.x*position+this.originalLeft+'px',top:this.options.y*position+this.originalTop+'px'});}});Effect.MoveBy=function(element,toTop,toLeft){return new Effect.Move(element,Object.extend({x:toLeft,y:toTop},arguments[3]||{}));};Effect.Scale=Class.create();Object.extend(Object.extend(Effect.Scale.prototype,Effect.Base.prototype),{initialize:function(element,percent){this.element=$(element)
+var options=Object.extend({scaleX:true,scaleY:true,scaleContent:true,scaleFromCenter:false,scaleMode:'box',scaleFrom:100.0,scaleTo:percent},arguments[2]||{});this.start(options);},setup:function(){this.restoreAfterFinish=this.options.restoreAfterFinish||false;this.elementPositioning=Element.getStyle(this.element,'position');this.originalStyle={};['top','left','width','height','fontSize'].each(function(k){this.originalStyle[k]=this.element.style[k];}.bind(this));this.originalTop=this.element.offsetTop;this.originalLeft=this.element.offsetLeft;var fontSize=Element.getStyle(this.element,'font-size')||'100%';['em','px','%'].each(function(fontSizeType){if(fontSize.indexOf(fontSizeType)>0){this.fontSize=parseFloat(fontSize);this.fontSizeType=fontSizeType;}}.bind(this));this.factor=(this.options.scaleTo-this.options.scaleFrom)/100;this.dims=null;if(this.options.scaleMode=='box')
+this.dims=[this.element.offsetHeight,this.element.offsetWidth];if(/^content/.test(this.options.scaleMode))
+this.dims=[this.element.scrollHeight,this.element.scrollWidth];if(!this.dims)
+this.dims=[this.options.scaleMode.originalHeight,this.options.scaleMode.originalWidth];},update:function(position){var currentScale=(this.options.scaleFrom/100.0)+(this.factor*position);if(this.options.scaleContent&&this.fontSize)
+Element.setStyle(this.element,{fontSize:this.fontSize*currentScale+this.fontSizeType});this.setDimensions(this.dims[0]*currentScale,this.dims[1]*currentScale);},finish:function(position){if(this.restoreAfterFinish)Element.setStyle(this.element,this.originalStyle);},setDimensions:function(height,width){var d={};if(this.options.scaleX)d.width=width+'px';if(this.options.scaleY)d.height=height+'px';if(this.options.scaleFromCenter){var topd=(height-this.dims[0])/2;var leftd=(width-this.dims[1])/2;if(this.elementPositioning=='absolute'){if(this.options.scaleY)d.top=this.originalTop-topd+'px';if(this.options.scaleX)d.left=this.originalLeft-leftd+'px';}else{if(this.options.scaleY)d.top=-topd+'px';if(this.options.scaleX)d.left=-leftd+'px';}}
+Element.setStyle(this.element,d);}});Effect.Highlight=Class.create();Object.extend(Object.extend(Effect.Highlight.prototype,Effect.Base.prototype),{initialize:function(element){this.element=$(element);var options=Object.extend({startcolor:'#ffff99'},arguments[1]||{});this.start(options);},setup:function(){if(Element.getStyle(this.element,'display')=='none'){this.cancel();return;}
+this.oldStyle={backgroundImage:Element.getStyle(this.element,'background-image')};Element.setStyle(this.element,{backgroundImage:'none'});if(!this.options.endcolor)
+this.options.endcolor=Element.getStyle(this.element,'background-color').parseColor('#ffffff');if(!this.options.restorecolor)
+this.options.restorecolor=Element.getStyle(this.element,'background-color');this._base=$R(0,2).map(function(i){return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16)}.bind(this));this._delta=$R(0,2).map(function(i){return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i]}.bind(this));},update:function(position){Element.setStyle(this.element,{backgroundColor:$R(0,2).inject('#',function(m,v,i){return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart());}.bind(this))});},finish:function(){Element.setStyle(this.element,Object.extend(this.oldStyle,{backgroundColor:this.options.restorecolor}));}});Effect.ScrollTo=Class.create();Object.extend(Object.extend(Effect.ScrollTo.prototype,Effect.Base.prototype),{initialize:function(element){this.element=$(element);this.start(arguments[1]||{});},setup:function(){Position.prepare();var offsets=Position.cumulativeOffset(this.element);if(this.options.offset)offsets[1]+=this.options.offset;var max=window.innerHeight?window.height-window.innerHeight:document.body.scrollHeight-
+(document.documentElement.clientHeight?document.documentElement.clientHeight:document.body.clientHeight);this.scrollStart=Position.deltaY;this.delta=(offsets[1]>max?max:offsets[1])-this.scrollStart;},update:function(position){Position.prepare();window.scrollTo(Position.deltaX,this.scrollStart+(position*this.delta));}});Effect.Fade=function(element){var oldOpacity=Element.getInlineOpacity(element);var options=Object.extend({from:Element.getOpacity(element)||1.0,to:0.0,afterFinishInternal:function(effect){with(Element){if(effect.options.to!=0)return;hide(effect.element);setStyle(effect.element,{opacity:oldOpacity});}}},arguments[1]||{});return new Effect.Opacity(element,options);}
+Effect.Appear=function(element){var options=Object.extend({from:(Element.getStyle(element,'display')=='none'?0.0:Element.getOpacity(element)||0.0),to:1.0,beforeSetup:function(effect){with(Element){setOpacity(effect.element,effect.options.from);show(effect.element);}}},arguments[1]||{});return new Effect.Opacity(element,options);}
+Effect.Puff=function(element){element=$(element);var oldStyle={opacity:Element.getInlineOpacity(element),position:Element.getStyle(element,'position')};return new Effect.Parallel([new Effect.Scale(element,200,{sync:true,scaleFromCenter:true,scaleContent:true,restoreAfterFinish:true}),new Effect.Opacity(element,{sync:true,to:0.0})],Object.extend({duration:1.0,beforeSetupInternal:function(effect){with(Element){setStyle(effect.effects[0].element,{position:'absolute'});}},afterFinishInternal:function(effect){with(Element){hide(effect.effects[0].element);setStyle(effect.effects[0].element,oldStyle);}}},arguments[1]||{}));}
+Effect.BlindUp=function(element){element=$(element);Element.makeClipping(element);return new Effect.Scale(element,0,Object.extend({scaleContent:false,scaleX:false,restoreAfterFinish:true,afterFinishInternal:function(effect){with(Element){[hide,undoClipping].call(effect.element);}}},arguments[1]||{}));}
+Effect.BlindDown=function(element){element=$(element);var oldHeight=Element.getStyle(element,'height');var elementDimensions=Element.getDimensions(element);return new Effect.Scale(element,100,Object.extend({scaleContent:false,scaleX:false,scaleFrom:0,scaleMode:{originalHeight:elementDimensions.height,originalWidth:elementDimensions.width},restoreAfterFinish:true,afterSetup:function(effect){with(Element){makeClipping(effect.element);setStyle(effect.element,{height:'0px'});show(effect.element);}},afterFinishInternal:function(effect){with(Element){undoClipping(effect.element);setStyle(effect.element,{height:oldHeight});}}},arguments[1]||{}));}
+Effect.SwitchOff=function(element){element=$(element);var oldOpacity=Element.getInlineOpacity(element);return new Effect.Appear(element,{duration:0.4,from:0,transition:Effect.Transitions.flicker,afterFinishInternal:function(effect){new Effect.Scale(effect.element,1,{duration:0.3,scaleFromCenter:true,scaleX:false,scaleContent:false,restoreAfterFinish:true,beforeSetup:function(effect){with(Element){[makePositioned,makeClipping].call(effect.element);}},afterFinishInternal:function(effect){with(Element){[hide,undoClipping,undoPositioned].call(effect.element);setStyle(effect.element,{opacity:oldOpacity});}}})}});}
+Effect.DropOut=function(element){element=$(element);var oldStyle={top:Element.getStyle(element,'top'),left:Element.getStyle(element,'left'),opacity:Element.getInlineOpacity(element)};return new Effect.Parallel([new Effect.Move(element,{x:0,y:100,sync:true}),new Effect.Opacity(element,{sync:true,to:0.0})],Object.extend({duration:0.5,beforeSetup:function(effect){with(Element){makePositioned(effect.effects[0].element);}},afterFinishInternal:function(effect){with(Element){[hide,undoPositioned].call(effect.effects[0].element);setStyle(effect.effects[0].element,oldStyle);}}},arguments[1]||{}));}
+Effect.Shake=function(element){element=$(element);var oldStyle={top:Element.getStyle(element,'top'),left:Element.getStyle(element,'left')};return new Effect.Move(element,{x:20,y:0,duration:0.05,afterFinishInternal:function(effect){new Effect.Move(effect.element,{x:-40,y:0,duration:0.1,afterFinishInternal:function(effect){new Effect.Move(effect.element,{x:40,y:0,duration:0.1,afterFinishInternal:function(effect){new Effect.Move(effect.element,{x:-40,y:0,duration:0.1,afterFinishInternal:function(effect){new Effect.Move(effect.element,{x:40,y:0,duration:0.1,afterFinishInternal:function(effect){new Effect.Move(effect.element,{x:-20,y:0,duration:0.05,afterFinishInternal:function(effect){with(Element){undoPositioned(effect.element);setStyle(effect.element,oldStyle);}}})}})}})}})}})}});}
+Effect.SlideDown=function(element){element=$(element);Element.cleanWhitespace(element);var oldInnerBottom=Element.getStyle(element.firstChild,'bottom');var elementDimensions=Element.getDimensions(element);return new Effect.Scale(element,100,Object.extend({scaleContent:false,scaleX:false,scaleFrom:0,scaleMode:{originalHeight:elementDimensions.height,originalWidth:elementDimensions.width},restoreAfterFinish:true,afterSetup:function(effect){with(Element){makePositioned(effect.element);makePositioned(effect.element.firstChild);if(window.opera)setStyle(effect.element,{top:''});makeClipping(effect.element);setStyle(effect.element,{height:'0px'});show(element);}},afterUpdateInternal:function(effect){with(Element){setStyle(effect.element.firstChild,{bottom:(effect.dims[0]-effect.element.clientHeight)+'px'});}},afterFinishInternal:function(effect){with(Element){undoClipping(effect.element);undoPositioned(effect.element.firstChild);undoPositioned(effect.element);setStyle(effect.element.firstChild,{bottom:oldInnerBottom});}}},arguments[1]||{}));}
+Effect.SlideUp=function(element){element=$(element);Element.cleanWhitespace(element);var oldInnerBottom=Element.getStyle(element.firstChild,'bottom');return new Effect.Scale(element,0,Object.extend({scaleContent:false,scaleX:false,scaleMode:'box',scaleFrom:100,restoreAfterFinish:true,beforeStartInternal:function(effect){with(Element){makePositioned(effect.element);makePositioned(effect.element.firstChild);if(window.opera)setStyle(effect.element,{top:''});makeClipping(effect.element);show(element);}},afterUpdateInternal:function(effect){with(Element){setStyle(effect.element.firstChild,{bottom:(effect.dims[0]-effect.element.clientHeight)+'px'});}},afterFinishInternal:function(effect){with(Element){[hide,undoClipping].call(effect.element);undoPositioned(effect.element.firstChild);undoPositioned(effect.element);setStyle(effect.element.firstChild,{bottom:oldInnerBottom});}}},arguments[1]||{}));}
+Effect.Squish=function(element){return new Effect.Scale(element,window.opera?1:0,{restoreAfterFinish:true,beforeSetup:function(effect){with(Element){makeClipping(effect.element);}},afterFinishInternal:function(effect){with(Element){hide(effect.element);undoClipping(effect.element);}}});}
+Effect.Grow=function(element){element=$(element);var options=Object.extend({direction:'center',moveTransistion:Effect.Transitions.sinoidal,scaleTransition:Effect.Transitions.sinoidal,opacityTransition:Effect.Transitions.full},arguments[1]||{});var oldStyle={top:element.style.top,left:element.style.left,height:element.style.height,width:element.style.width,opacity:Element.getInlineOpacity(element)};var dims=Element.getDimensions(element);var initialMoveX,initialMoveY;var moveX,moveY;switch(options.direction){case'top-left':initialMoveX=initialMoveY=moveX=moveY=0;break;case'top-right':initialMoveX=dims.width;initialMoveY=moveY=0;moveX=-dims.width;break;case'bottom-left':initialMoveX=moveX=0;initialMoveY=dims.height;moveY=-dims.height;break;case'bottom-right':initialMoveX=dims.width;initialMoveY=dims.height;moveX=-dims.width;moveY=-dims.height;break;case'center':initialMoveX=dims.width/2;initialMoveY=dims.height/2;moveX=-dims.width/2;moveY=-dims.height/2;break;}
+return new Effect.Move(element,{x:initialMoveX,y:initialMoveY,duration:0.01,beforeSetup:function(effect){with(Element){hide(effect.element);makeClipping(effect.element);makePositioned(effect.element);}},afterFinishInternal:function(effect){new Effect.Parallel([new Effect.Opacity(effect.element,{sync:true,to:1.0,from:0.0,transition:options.opacityTransition}),new Effect.Move(effect.element,{x:moveX,y:moveY,sync:true,transition:options.moveTransition}),new Effect.Scale(effect.element,100,{scaleMode:{originalHeight:dims.height,originalWidth:dims.width},sync:true,scaleFrom:window.opera?1:0,transition:options.scaleTransition,restoreAfterFinish:true})],Object.extend({beforeSetup:function(effect){with(Element){setStyle(effect.effects[0].element,{height:'0px'});show(effect.effects[0].element);}},afterFinishInternal:function(effect){with(Element){[undoClipping,undoPositioned].call(effect.effects[0].element);setStyle(effect.effects[0].element,oldStyle);}}},options))}});}
+Effect.Shrink=function(element){element=$(element);var options=Object.extend({direction:'center',moveTransistion:Effect.Transitions.sinoidal,scaleTransition:Effect.Transitions.sinoidal,opacityTransition:Effect.Transitions.none},arguments[1]||{});var oldStyle={top:element.style.top,left:element.style.left,height:element.style.height,width:element.style.width,opacity:Element.getInlineOpacity(element)};var dims=Element.getDimensions(element);var moveX,moveY;switch(options.direction){case'top-left':moveX=moveY=0;break;case'top-right':moveX=dims.width;moveY=0;break;case'bottom-left':moveX=0;moveY=dims.height;break;case'bottom-right':moveX=dims.width;moveY=dims.height;break;case'center':moveX=dims.width/2;moveY=dims.height/2;break;}
+return new Effect.Parallel([new Effect.Opacity(element,{sync:true,to:0.0,from:1.0,transition:options.opacityTransition}),new Effect.Scale(element,window.opera?1:0,{sync:true,transition:options.scaleTransition,restoreAfterFinish:true}),new Effect.Move(element,{x:moveX,y:moveY,sync:true,transition:options.moveTransition})],Object.extend({beforeStartInternal:function(effect){with(Element){[makePositioned,makeClipping].call(effect.effects[0].element)}},afterFinishInternal:function(effect){with(Element){[hide,undoClipping,undoPositioned].call(effect.effects[0].element);setStyle(effect.effects[0].element,oldStyle);}}},options));}
+Effect.Pulsate=function(element){element=$(element);var options=arguments[1]||{};var oldOpacity=Element.getInlineOpacity(element);var transition=options.transition||Effect.Transitions.sinoidal;var reverser=function(pos){return transition(1-Effect.Transitions.pulse(pos))};reverser.bind(transition);return new Effect.Opacity(element,Object.extend(Object.extend({duration:3.0,from:0,afterFinishInternal:function(effect){Element.setStyle(effect.element,{opacity:oldOpacity});}},options),{transition:reverser}));}
+Effect.Fold=function(element){element=$(element);var oldStyle={top:element.style.top,left:element.style.left,width:element.style.width,height:element.style.height};Element.makeClipping(element);return new Effect.Scale(element,5,Object.extend({scaleContent:false,scaleX:false,afterFinishInternal:function(effect){new Effect.Scale(element,1,{scaleContent:false,scaleY:false,afterFinishInternal:function(effect){with(Element){[hide,undoClipping].call(effect.element);setStyle(effect.element,oldStyle);}}});}},arguments[1]||{}));}
\ No newline at end of file
diff --git a/babel/js/prototype.js b/babel/js/prototype.js
new file mode 100644 (file)
index 0000000..0bb8807
--- /dev/null
@@ -0,0 +1,4221 @@
+/*  Prototype JavaScript framework, version 1.6.0.2
+ *  (c) 2005-2008 Sam Stephenson
+ *
+ *  Prototype is freely distributable under the terms of an MIT-style license.
+ *  For details, see the Prototype web site: http://www.prototypejs.org/
+ *
+ *--------------------------------------------------------------------------*/
+
+var Prototype = {
+  Version: '1.6.0.2',
+
+  Browser: {
+    IE:     !!(window.attachEvent && !window.opera),
+    Opera:  !!window.opera,
+    WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
+    Gecko:  navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1,
+    MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
+  },
+
+  BrowserFeatures: {
+    XPath: !!document.evaluate,
+    ElementExtensions: !!window.HTMLElement,
+    SpecificElementExtensions:
+      document.createElement('div').__proto__ &&
+      document.createElement('div').__proto__ !==
+        document.createElement('form').__proto__
+  },
+
+  ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
+  JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,
+
+  emptyFunction: function() { },
+  K: function(x) { return x }
+};
+
+if (Prototype.Browser.MobileSafari)
+  Prototype.BrowserFeatures.SpecificElementExtensions = false;
+
+
+/* Based on Alex Arnell's inheritance implementation. */
+var Class = {
+  create: function() {
+    var parent = null, properties = $A(arguments);
+    if (Object.isFunction(properties[0]))
+      parent = properties.shift();
+
+    function klass() {
+      this.initialize.apply(this, arguments);
+    }
+
+    Object.extend(klass, Class.Methods);
+    klass.superclass = parent;
+    klass.subclasses = [];
+
+    if (parent) {
+      var subclass = function() { };
+      subclass.prototype = parent.prototype;
+      klass.prototype = new subclass;
+      parent.subclasses.push(klass);
+    }
+
+    for (var i = 0; i < properties.length; i++)
+      klass.addMethods(properties[i]);
+
+    if (!klass.prototype.initialize)
+      klass.prototype.initialize = Prototype.emptyFunction;
+
+    klass.prototype.constructor = klass;
+
+    return klass;
+  }
+};
+
+Class.Methods = {
+  addMethods: function(source) {
+    var ancestor   = this.superclass && this.superclass.prototype;
+    var properties = Object.keys(source);
+
+    if (!Object.keys({ toString: true }).length)
+      properties.push("toString", "valueOf");
+
+    for (var i = 0, length = properties.length; i < length; i++) {
+      var property = properties[i], value = source[property];
+      if (ancestor && Object.isFunction(value) &&
+          value.argumentNames().first() == "$super") {
+        var method = value, value = Object.extend((function(m) {
+          return function() { return ancestor[m].apply(this, arguments) };
+        })(property).wrap(method), {
+          valueOf:  function() { return method },
+          toString: function() { return method.toString() }
+        });
+      }
+      this.prototype[property] = value;
+    }
+
+    return this;
+  }
+};
+
+var Abstract = { };
+
+Object.extend = function(destination, source) {
+  for (var property in source)
+    destination[property] = source[property];
+  return destination;
+};
+
+Object.extend(Object, {
+  inspect: function(object) {
+    try {
+      if (Object.isUndefined(object)) return 'undefined';
+      if (object === null) return 'null';
+      return object.inspect ? object.inspect() : String(object);
+    } catch (e) {
+      if (e instanceof RangeError) return '...';
+      throw e;
+    }
+  },
+
+  toJSON: function(object) {
+    var type = typeof object;
+    switch (type) {
+      case 'undefined':
+      case 'function':
+      case 'unknown': return;
+      case 'boolean': return object.toString();
+    }
+
+    if (object === null) return 'null';
+    if (object.toJSON) return object.toJSON();
+    if (Object.isElement(object)) return;
+
+    var results = [];
+    for (var property in object) {
+      var value = Object.toJSON(object[property]);
+      if (!Object.isUndefined(value))
+        results.push(property.toJSON() + ': ' + value);
+    }
+
+    return '{' + results.join(', ') + '}';
+  },
+
+  toQueryString: function(object) {
+    return $H(object).toQueryString();
+  },
+
+  toHTML: function(object) {
+    return object && object.toHTML ? object.toHTML() : String.interpret(object);
+  },
+
+  keys: function(object) {
+    var keys = [];
+    for (var property in object)
+      keys.push(property);
+    return keys;
+  },
+
+  values: function(object) {
+    var values = [];
+    for (var property in object)
+      values.push(object[property]);
+    return values;
+  },
+
+  clone: function(object) {
+    return Object.extend({ }, object);
+  },
+
+  isElement: function(object) {
+    return object && object.nodeType == 1;
+  },
+
+  isArray: function(object) {
+    return object != null && typeof object == "object" &&
+      'splice' in object && 'join' in object;
+  },
+
+  isHash: function(object) {
+    return object instanceof Hash;
+  },
+
+  isFunction: function(object) {
+    return typeof object == "function";
+  },
+
+  isString: function(object) {
+    return typeof object == "string";
+  },
+
+  isNumber: function(object) {
+    return typeof object == "number";
+  },
+
+  isUndefined: function(object) {
+    return typeof object == "undefined";
+  }
+});
+
+Object.extend(Function.prototype, {
+  argumentNames: function() {
+    var names = this.toString().match(/^[\s\(]*function[^(]*\((.*?)\)/)[1].split(",").invoke("strip");
+    return names.length == 1 && !names[0] ? [] : names;
+  },
+
+  bind: function() {
+    if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
+    var __method = this, args = $A(arguments), object = args.shift();
+    return function() {
+      return __method.apply(object, args.concat($A(arguments)));
+    }
+  },
+
+  bindAsEventListener: function() {
+    var __method = this, args = $A(arguments), object = args.shift();
+    return function(event) {
+      return __method.apply(object, [event || window.event].concat(args));
+    }
+  },
+
+  curry: function() {
+    if (!arguments.length) return this;
+    var __method = this, args = $A(arguments);
+    return function() {
+      return __method.apply(this, args.concat($A(arguments)));
+    }
+  },
+
+  delay: function() {
+    var __method = this, args = $A(arguments), timeout = args.shift() * 1000;
+    return window.setTimeout(function() {
+      return __method.apply(__method, args);
+    }, timeout);
+  },
+
+  wrap: function(wrapper) {
+    var __method = this;
+    return function() {
+      return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));
+    }
+  },
+
+  methodize: function() {
+    if (this._methodized) return this._methodized;
+    var __method = this;
+    return this._methodized = function() {
+      return __method.apply(null, [this].concat($A(arguments)));
+    };
+  }
+});
+
+Function.prototype.defer = Function.prototype.delay.curry(0.01);
+
+Date.prototype.toJSON = function() {
+  return '"' + this.getUTCFullYear() + '-' +
+    (this.getUTCMonth() + 1).toPaddedString(2) + '-' +
+    this.getUTCDate().toPaddedString(2) + 'T' +
+    this.getUTCHours().toPaddedString(2) + ':' +
+    this.getUTCMinutes().toPaddedString(2) + ':' +
+    this.getUTCSeconds().toPaddedString(2) + 'Z"';
+};
+
+var Try = {
+  these: function() {
+    var returnValue;
+
+    for (var i = 0, length = arguments.length; i < length; i++) {
+      var lambda = arguments[i];
+      try {
+        returnValue = lambda();
+        break;
+      } catch (e) { }
+    }
+
+    return returnValue;
+  }
+};
+
+RegExp.prototype.match = RegExp.prototype.test;
+
+RegExp.escape = function(str) {
+  return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
+};
+
+/*--------------------------------------------------------------------------*/
+
+var PeriodicalExecuter = Class.create({
+  initialize: function(callback, frequency) {
+    this.callback = callback;
+    this.frequency = frequency;
+    this.currentlyExecuting = false;
+
+    this.registerCallback();
+  },
+
+  registerCallback: function() {
+    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+  },
+
+  execute: function() {
+    this.callback(this);
+  },
+
+  stop: function() {
+    if (!this.timer) return;
+    clearInterval(this.timer);
+    this.timer = null;
+  },
+
+  onTimerEvent: function() {
+    if (!this.currentlyExecuting) {
+      try {
+        this.currentlyExecuting = true;
+        this.execute();
+      } finally {
+        this.currentlyExecuting = false;
+      }
+    }
+  }
+});
+Object.extend(String, {
+  interpret: function(value) {
+    return value == null ? '' : String(value);
+  },
+  specialChar: {
+    '\b': '\\b',
+    '\t': '\\t',
+    '\n': '\\n',
+    '\f': '\\f',
+    '\r': '\\r',
+    '\\': '\\\\'
+  }
+});
+
+Object.extend(String.prototype, {
+  gsub: function(pattern, replacement) {
+    var result = '', source = this, match;
+    replacement = arguments.callee.prepareReplacement(replacement);
+
+    while (source.length > 0) {
+      if (match = source.match(pattern)) {
+        result += source.slice(0, match.index);
+        result += String.interpret(replacement(match));
+        source  = source.slice(match.index + match[0].length);
+      } else {
+        result += source, source = '';
+      }
+    }
+    return result;
+  },
+
+  sub: function(pattern, replacement, count) {
+    replacement = this.gsub.prepareReplacement(replacement);
+    count = Object.isUndefined(count) ? 1 : count;
+
+    return this.gsub(pattern, function(match) {
+      if (--count < 0) return match[0];
+      return replacement(match);
+    });
+  },
+
+  scan: function(pattern, iterator) {
+    this.gsub(pattern, iterator);
+    return String(this);
+  },
+
+  truncate: function(length, truncation) {
+    length = length || 30;
+    truncation = Object.isUndefined(truncation) ? '...' : truncation;
+    return this.length > length ?
+      this.slice(0, length - truncation.length) + truncation : String(this);
+  },
+
+  strip: function() {
+    return this.replace(/^\s+/, '').replace(/\s+$/, '');
+  },
+
+  stripTags: function() {
+    return this.replace(/<\/?[^>]+>/gi, '');
+  },
+
+  stripScripts: function() {
+    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
+  },
+
+  extractScripts: function() {
+    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
+    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
+    return (this.match(matchAll) || []).map(function(scriptTag) {
+      return (scriptTag.match(matchOne) || ['', ''])[1];
+    });
+  },
+
+  evalScripts: function() {
+    return this.extractScripts().map(function(script) { return eval(script) });
+  },
+
+  escapeHTML: function() {
+    var self = arguments.callee;
+    self.text.data = this;
+    return self.div.innerHTML;
+  },
+
+  unescapeHTML: function() {
+    var div = new Element('div');
+    div.innerHTML = this.stripTags();
+    return div.childNodes[0] ? (div.childNodes.length > 1 ?
+      $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
+      div.childNodes[0].nodeValue) : '';
+  },
+
+  toQueryParams: function(separator) {
+    var match = this.strip().match(/([^?#]*)(#.*)?$/);
+    if (!match) return { };
+
+    return match[1].split(separator || '&').inject({ }, function(hash, pair) {
+      if ((pair = pair.split('='))[0]) {
+        var key = decodeURIComponent(pair.shift());
+        var value = pair.length > 1 ? pair.join('=') : pair[0];
+        if (value != undefined) value = decodeURIComponent(value);
+
+        if (key in hash) {
+          if (!Object.isArray(hash[key])) hash[key] = [hash[key]];
+          hash[key].push(value);
+        }
+        else hash[key] = value;
+      }
+      return hash;
+    });
+  },
+
+  toArray: function() {
+    return this.split('');
+  },
+
+  succ: function() {
+    return this.slice(0, this.length - 1) +
+      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
+  },
+
+  times: function(count) {
+    return count < 1 ? '' : new Array(count + 1).join(this);
+  },
+
+  camelize: function() {
+    var parts = this.split('-'), len = parts.length;
+    if (len == 1) return parts[0];
+
+    var camelized = this.charAt(0) == '-'
+      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
+      : parts[0];
+
+    for (var i = 1; i < len; i++)
+      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
+
+    return camelized;
+  },
+
+  capitalize: function() {
+    return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
+  },
+
+  underscore: function() {
+    return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
+  },
+
+  dasherize: function() {
+    return this.gsub(/_/,'-');
+  },
+
+  inspect: function(useDoubleQuotes) {
+    var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
+      var character = String.specialChar[match[0]];
+      return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
+    });
+    if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
+    return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+  },
+
+  toJSON: function() {
+    return this.inspect(true);
+  },
+
+  unfilterJSON: function(filter) {
+    return this.sub(filter || Prototype.JSONFilter, '#{1}');
+  },
+
+  isJSON: function() {
+    var str = this;
+    if (str.blank()) return false;
+    str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
+    return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
+  },
+
+  evalJSON: function(sanitize) {
+    var json = this.unfilterJSON();
+    try {
+      if (!sanitize || json.isJSON()) return eval('(' + json + ')');
+    } catch (e) { }
+    throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
+  },
+
+  include: function(pattern) {
+    return this.indexOf(pattern) > -1;
+  },
+
+  startsWith: function(pattern) {
+    return this.indexOf(pattern) === 0;
+  },
+
+  endsWith: function(pattern) {
+    var d = this.length - pattern.length;
+    return d >= 0 && this.lastIndexOf(pattern) === d;
+  },
+
+  empty: function() {
+    return this == '';
+  },
+
+  blank: function() {
+    return /^\s*$/.test(this);
+  },
+
+  interpolate: function(object, pattern) {
+    return new Template(this, pattern).evaluate(object);
+  }
+});
+
+if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
+  escapeHTML: function() {
+    return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+  },
+  unescapeHTML: function() {
+    return this.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
+  }
+});
+
+String.prototype.gsub.prepareReplacement = function(replacement) {
+  if (Object.isFunction(replacement)) return replacement;
+  var template = new Template(replacement);
+  return function(match) { return template.evaluate(match) };
+};
+
+String.prototype.parseQuery = String.prototype.toQueryParams;
+
+Object.extend(String.prototype.escapeHTML, {
+  div:  document.createElement('div'),
+  text: document.createTextNode('')
+});
+
+with (String.prototype.escapeHTML) div.appendChild(text);
+
+var Template = Class.create({
+  initialize: function(template, pattern) {
+    this.template = template.toString();
+    this.pattern = pattern || Template.Pattern;
+  },
+
+  evaluate: function(object) {
+    if (Object.isFunction(object.toTemplateReplacements))
+      object = object.toTemplateReplacements();
+
+    return this.template.gsub(this.pattern, function(match) {
+      if (object == null) return '';
+
+      var before = match[1] || '';
+      if (before == '\\') return match[2];
+
+      var ctx = object, expr = match[3];
+      var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;
+      match = pattern.exec(expr);
+      if (match == null) return before;
+
+      while (match != null) {
+        var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1];
+        ctx = ctx[comp];
+        if (null == ctx || '' == match[3]) break;
+        expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);
+        match = pattern.exec(expr);
+      }
+
+      return before + String.interpret(ctx);
+    });
+  }
+});
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+
+var $break = { };
+
+var Enumerable = {
+  each: function(iterator, context) {
+    var index = 0;
+    iterator = iterator.bind(context);
+    try {
+      this._each(function(value) {
+        iterator(value, index++);
+      });
+    } catch (e) {
+      if (e != $break) throw e;
+    }
+    return this;
+  },
+
+  eachSlice: function(number, iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
+    var index = -number, slices = [], array = this.toArray();
+    while ((index += number) < array.length)
+      slices.push(array.slice(index, index+number));
+    return slices.collect(iterator, context);
+  },
+
+  all: function(iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
+    var result = true;
+    this.each(function(value, index) {
+      result = result && !!iterator(value, index);
+      if (!result) throw $break;
+    });
+    return result;
+  },
+
+  any: function(iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
+    var result = false;
+    this.each(function(value, index) {
+      if (result = !!iterator(value, index))
+        throw $break;
+    });
+    return result;
+  },
+
+  collect: function(iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
+    var results = [];
+    this.each(function(value, index) {
+      results.push(iterator(value, index));
+    });
+    return results;
+  },
+
+  detect: function(iterator, context) {
+    iterator = iterator.bind(context);
+    var result;
+    this.each(function(value, index) {
+      if (iterator(value, index)) {
+        result = value;
+        throw $break;
+      }
+    });
+    return result;
+  },
+
+  findAll: function(iterator, context) {
+    iterator = iterator.bind(context);
+    var results = [];
+    this.each(function(value, index) {
+      if (iterator(value, index))
+        results.push(value);
+    });
+    return results;
+  },
+
+  grep: function(filter, iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
+    var results = [];
+
+    if (Object.isString(filter))
+      filter = new RegExp(filter);
+
+    this.each(function(value, index) {
+      if (filter.match(value))
+        results.push(iterator(value, index));
+    });
+    return results;
+  },
+
+  include: function(object) {
+    if (Object.isFunction(this.indexOf))
+      if (this.indexOf(object) != -1) return true;
+
+    var found = false;
+    this.each(function(value) {
+      if (value == object) {
+        found = true;
+        throw $break;
+      }
+    });
+    return found;
+  },
+
+  inGroupsOf: function(number, fillWith) {
+    fillWith = Object.isUndefined(fillWith) ? null : fillWith;
+    return this.eachSlice(number, function(slice) {
+      while(slice.length < number) slice.push(fillWith);
+      return slice;
+    });
+  },
+
+  inject: function(memo, iterator, context) {
+    iterator = iterator.bind(context);
+    this.each(function(value, index) {
+      memo = iterator(memo, value, index);
+    });
+    return memo;
+  },
+
+  invoke: function(method) {
+    var args = $A(arguments).slice(1);
+    return this.map(function(value) {
+      return value[method].apply(value, args);
+    });
+  },
+
+  max: function(iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
+    var result;
+    this.each(function(value, index) {
+      value = iterator(value, index);
+      if (result == null || value >= result)
+        result = value;
+    });
+    return result;
+  },
+
+  min: function(iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
+    var result;
+    this.each(function(value, index) {
+      value = iterator(value, index);
+      if (result == null || value < result)
+        result = value;
+    });
+    return result;
+  },
+
+  partition: function(iterator, context) {
+    iterator = iterator ? iterator.bind(context) : Prototype.K;
+    var trues = [], falses = [];
+    this.each(function(value, index) {
+      (iterator(value, index) ?
+        trues : falses).push(value);
+    });
+    return [trues, falses];
+  },
+
+  pluck: function(property) {
+    var results = [];
+    this.each(function(value) {
+      results.push(value[property]);
+    });
+    return results;
+  },
+
+  reject: function(iterator, context) {
+    iterator = iterator.bind(context);
+    var results = [];
+    this.each(function(value, index) {
+      if (!iterator(value, index))
+        results.push(value);
+    });
+    return results;
+  },
+
+  sortBy: function(iterator, context) {
+    iterator = iterator.bind(context);
+    return this.map(function(value, index) {
+      return {value: value, criteria: iterator(value, index)};
+    }).sort(function(left, right) {
+      var a = left.criteria, b = right.criteria;
+      return a < b ? -1 : a > b ? 1 : 0;
+    }).pluck('value');
+  },
+
+  toArray: function() {
+    return this.map();
+  },
+
+  zip: function() {
+    var iterator = Prototype.K, args = $A(arguments);
+    if (Object.isFunction(args.last()))
+      iterator = args.pop();
+
+    var collections = [this].concat(args).map($A);
+    return this.map(function(value, index) {
+      return iterator(collections.pluck(index));
+    });
+  },
+
+  size: function() {
+    return this.toArray().length;
+  },
+
+  inspect: function() {
+    return '#<Enumerable:' + this.toArray().inspect() + '>';
+  }
+};
+
+Object.extend(Enumerable, {
+  map:     Enumerable.collect,
+  find:    Enumerable.detect,
+  select:  Enumerable.findAll,
+  filter:  Enumerable.findAll,
+  member:  Enumerable.include,
+  entries: Enumerable.toArray,
+  every:   Enumerable.all,
+  some:    Enumerable.any
+});
+function $A(iterable) {
+  if (!iterable) return [];
+  if (iterable.toArray) return iterable.toArray();
+  var length = iterable.length || 0, results = new Array(length);
+  while (length--) results[length] = iterable[length];
+  return results;
+}
+
+if (Prototype.Browser.WebKit) {
+  $A = function(iterable) {
+    if (!iterable) return [];
+    if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') &&
+        iterable.toArray) return iterable.toArray();
+    var length = iterable.length || 0, results = new Array(length);
+    while (length--) results[length] = iterable[length];
+    return results;
+  };
+}
+
+Array.from = $A;
+
+Object.extend(Array.prototype, Enumerable);
+
+if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse;
+
+Object.extend(Array.prototype, {
+  _each: function(iterator) {
+    for (var i = 0, length = this.length; i < length; i++)
+      iterator(this[i]);
+  },
+
+  clear: function() {
+    this.length = 0;
+    return this;
+  },
+
+  first: function() {
+    return this[0];
+  },
+
+  last: function() {
+    return this[this.length - 1];
+  },
+
+  compact: function() {
+    return this.select(function(value) {
+      return value != null;
+    });
+  },
+
+  flatten: function() {
+    return this.inject([], function(array, value) {
+      return array.concat(Object.isArray(value) ?
+        value.flatten() : [value]);
+    });
+  },
+
+  without: function() {
+    var values = $A(arguments);
+    return this.select(function(value) {
+      return !values.include(value);
+    });
+  },
+
+  reverse: function(inline) {
+    return (inline !== false ? this : this.toArray())._reverse();
+  },
+
+  reduce: function() {
+    return this.length > 1 ? this : this[0];
+  },
+
+  uniq: function(sorted) {
+    return this.inject([], function(array, value, index) {
+      if (0 == index || (sorted ? array.last() != value : !array.include(value)))
+        array.push(value);
+      return array;
+    });
+  },
+
+  intersect: function(array) {
+    return this.uniq().findAll(function(item) {
+      return array.detect(function(value) { return item === value });
+    });
+  },
+
+  clone: function() {
+    return [].concat(this);
+  },
+
+  size: function() {
+    return this.length;
+  },
+
+  inspect: function() {
+    return '[' + this.map(Object.inspect).join(', ') + ']';
+  },
+
+  toJSON: function() {
+    var results = [];
+    this.each(function(object) {
+      var value = Object.toJSON(object);
+      if (!Object.isUndefined(value)) results.push(value);
+    });
+    return '[' + results.join(', ') + ']';
+  }
+});
+
+// use native browser JS 1.6 implementation if available
+if (Object.isFunction(Array.prototype.forEach))
+  Array.prototype._each = Array.prototype.forEach;
+
+if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) {
+  i || (i = 0);
+  var length = this.length;
+  if (i < 0) i = length + i;
+  for (; i < length; i++)
+    if (this[i] === item) return i;
+  return -1;
+};
+
+if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) {
+  i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;
+  var n = this.slice(0, i).reverse().indexOf(item);
+  return (n < 0) ? n : i - n - 1;
+};
+
+Array.prototype.toArray = Array.prototype.clone;
+
+function $w(string) {
+  if (!Object.isString(string)) return [];
+  string = string.strip();
+  return string ? string.split(/\s+/) : [];
+}
+
+if (Prototype.Browser.Opera){
+  Array.prototype.concat = function() {
+    var array = [];
+    for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
+    for (var i = 0, length = arguments.length; i < length; i++) {
+      if (Object.isArray(arguments[i])) {
+        for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
+          array.push(arguments[i][j]);
+      } else {
+        array.push(arguments[i]);
+      }
+    }
+    return array;
+  };
+}
+Object.extend(Number.prototype, {
+  toColorPart: function() {
+    return this.toPaddedString(2, 16);
+  },
+
+  succ: function() {
+    return this + 1;
+  },
+
+  times: function(iterator) {
+    $R(0, this, true).each(iterator);
+    return this;
+  },
+
+  toPaddedString: function(length, radix) {
+    var string = this.toString(radix || 10);
+    return '0'.times(length - string.length) + string;
+  },
+
+  toJSON: function() {
+    return isFinite(this) ? this.toString() : 'null';
+  }
+});
+
+$w('abs round ceil floor').each(function(method){
+  Number.prototype[method] = Math[method].methodize();
+});
+function $H(object) {
+  return new Hash(object);
+};
+
+var Hash = Class.create(Enumerable, (function() {
+
+  function toQueryPair(key, value) {
+    if (Object.isUndefined(value)) return key;
+    return key + '=' + encodeURIComponent(String.interpret(value));
+  }
+
+  return {
+    initialize: function(object) {
+      this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
+    },
+
+    _each: function(iterator) {
+      for (var key in this._object) {
+        var value = this._object[key], pair = [key, value];
+        pair.key = key;
+        pair.value = value;
+        iterator(pair);
+      }
+    },
+
+    set: function(key, value) {
+      return this._object[key] = value;
+    },
+
+    get: function(key) {
+      return this._object[key];
+    },
+
+    unset: function(key) {
+      var value = this._object[key];
+      delete this._object[key];
+      return value;
+    },
+
+    toObject: function() {
+      return Object.clone(this._object);
+    },
+
+    keys: function() {
+      return this.pluck('key');
+    },
+
+    values: function() {
+      return this.pluck('value');
+    },
+
+    index: function(value) {
+      var match = this.detect(function(pair) {
+        return pair.value === value;
+      });
+      return match && match.key;
+    },
+
+    merge: function(object) {
+      return this.clone().update(object);
+    },
+
+    update: function(object) {
+      return new Hash(object).inject(this, function(result, pair) {
+        result.set(pair.key, pair.value);
+        return result;
+      });
+    },
+
+    toQueryString: function() {
+      return this.map(function(pair) {
+        var key = encodeURIComponent(pair.key), values = pair.value;
+
+        if (values && typeof values == 'object') {
+          if (Object.isArray(values))
+            return values.map(toQueryPair.curry(key)).join('&');
+        }
+        return toQueryPair(key, values);
+      }).join('&');
+    },
+
+    inspect: function() {
+      return '#<Hash:{' + this.map(function(pair) {
+        return pair.map(Object.inspect).join(': ');
+      }).join(', ') + '}>';
+    },
+
+    toJSON: function() {
+      return Object.toJSON(this.toObject());
+    },
+
+    clone: function() {
+      return new Hash(this);
+    }
+  }
+})());
+
+Hash.prototype.toTemplateReplacements = Hash.prototype.toObject;
+Hash.from = $H;
+var ObjectRange = Class.create(Enumerable, {
+  initialize: function(start, end, exclusive) {
+    this.start = start;
+    this.end = end;
+    this.exclusive = exclusive;
+  },
+
+  _each: function(iterator) {
+    var value = this.start;
+    while (this.include(value)) {
+      iterator(value);
+      value = value.succ();
+    }
+  },
+
+  include: function(value) {
+    if (value < this.start)
+      return false;
+    if (this.exclusive)
+      return value < this.end;
+    return value <= this.end;
+  }
+});
+
+var $R = function(start, end, exclusive) {
+  return new ObjectRange(start, end, exclusive);
+};
+
+var Ajax = {
+  getTransport: function() {
+    return Try.these(
+      function() {return new XMLHttpRequest()},
+      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
+    ) || false;
+  },
+
+  activeRequestCount: 0
+};
+
+Ajax.Responders = {
+  responders: [],
+
+  _each: function(iterator) {
+    this.responders._each(iterator);
+  },
+
+  register: function(responder) {
+    if (!this.include(responder))
+      this.responders.push(responder);
+  },
+
+  unregister: function(responder) {
+    this.responders = this.responders.without(responder);
+  },
+
+  dispatch: function(callback, request, transport, json) {
+    this.each(function(responder) {
+      if (Object.isFunction(responder[callback])) {
+        try {
+          responder[callback].apply(responder, [request, transport, json]);
+        } catch (e) { }
+      }
+    });
+  }
+};
+
+Object.extend(Ajax.Responders, Enumerable);
+
+Ajax.Responders.register({
+  onCreate:   function() { Ajax.activeRequestCount++ },
+  onComplete: function() { Ajax.activeRequestCount-- }
+});
+
+Ajax.Base = Class.create({
+  initialize: function(options) {
+    this.options = {
+      method:       'post',
+      asynchronous: true,
+      contentType:  'application/x-www-form-urlencoded',
+      encoding:     'UTF-8',
+      parameters:   '',
+      evalJSON:     true,
+      evalJS:       true
+    };
+    Object.extend(this.options, options || { });
+
+    this.options.method = this.options.method.toLowerCase();
+
+    if (Object.isString(this.options.parameters))
+      this.options.parameters = this.options.parameters.toQueryParams();
+    else if (Object.isHash(this.options.parameters))
+      this.options.parameters = this.options.parameters.toObject();
+  }
+});
+
+Ajax.Request = Class.create(Ajax.Base, {
+  _complete: false,
+
+  initialize: function($super, url, options) {
+    $super(options);
+    this.transport = Ajax.getTransport();
+    this.request(url);
+  },
+
+  request: function(url) {
+    this.url = url;
+    this.method = this.options.method;
+    var params = Object.clone(this.options.parameters);
+
+    if (!['get', 'post'].include(this.method)) {
+      // simulate other verbs over post
+      params['_method'] = this.method;
+      this.method = 'post';
+    }
+
+    this.parameters = params;
+
+    if (params = Object.toQueryString(params)) {
+      // when GET, append parameters to URL
+      if (this.method == 'get')
+        this.url += (this.url.include('?') ? '&' : '?') + params;
+      else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
+        params += '&_=';
+    }
+
+    try {
+      var response = new Ajax.Response(this);
+      if (this.options.onCreate) this.options.onCreate(response);
+      Ajax.Responders.dispatch('onCreate', this, response);
+
+      this.transport.open(this.method.toUpperCase(), this.url,
+        this.options.asynchronous);
+
+      if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);
+
+      this.transport.onreadystatechange = this.onStateChange.bind(this);
+      this.setRequestHeaders();
+
+      this.body = this.method == 'post' ? (this.options.postBody || params) : null;
+      this.transport.send(this.body);
+
+      /* Force Firefox to handle ready state 4 for synchronous requests */
+      if (!this.options.asynchronous && this.transport.overrideMimeType)
+        this.onStateChange();
+
+    }
+    catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  onStateChange: function() {
+    var readyState = this.transport.readyState;
+    if (readyState > 1 && !((readyState == 4) && this._complete))
+      this.respondToReadyState(this.transport.readyState);
+  },
+
+  setRequestHeaders: function() {
+    var headers = {
+      'X-Requested-With': 'XMLHttpRequest',
+      'X-Prototype-Version': Prototype.Version,
+      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
+    };
+
+    if (this.method == 'post') {
+      headers['Content-type'] = this.options.contentType +
+        (this.options.encoding ? '; charset=' + this.options.encoding : '');
+
+      /* Force "Connection: close" for older Mozilla browsers to work
+       * around a bug where XMLHttpRequest sends an incorrect
+       * Content-length header. See Mozilla Bugzilla #246651.
+       */
+      if (this.transport.overrideMimeType &&
+          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
+            headers['Connection'] = 'close';
+    }
+
+    // user-defined headers
+    if (typeof this.options.requestHeaders == 'object') {
+      var extras = this.options.requestHeaders;
+
+      if (Object.isFunction(extras.push))
+        for (var i = 0, length = extras.length; i < length; i += 2)
+          headers[extras[i]] = extras[i+1];
+      else
+        $H(extras).each(function(pair) { headers[pair.key] = pair.value });
+    }
+
+    for (var name in headers)
+      this.transport.setRequestHeader(name, headers[name]);
+  },
+
+  success: function() {
+    var status = this.getStatus();
+    return !status || (status >= 200 && status < 300);
+  },
+
+  getStatus: function() {
+    try {
+      return this.transport.status || 0;
+    } catch (e) { return 0 }
+  },
+
+  respondToReadyState: function(readyState) {
+    var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);
+
+    if (state == 'Complete') {
+      try {
+        this._complete = true;
+        (this.options['on' + response.status]
+         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
+         || Prototype.emptyFunction)(response, response.headerJSON);
+      } catch (e) {
+        this.dispatchException(e);
+      }
+
+      var contentType = response.getHeader('Content-type');
+      if (this.options.evalJS == 'force'
+          || (this.options.evalJS && this.isSameOrigin() && contentType
+          && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
+        this.evalResponse();
+    }
+
+    try {
+      (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
+      Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
+    } catch (e) {
+      this.dispatchException(e);
+    }
+
+    if (state == 'Complete') {
+      // avoid memory leak in MSIE: clean up
+      this.transport.onreadystatechange = Prototype.emptyFunction;
+    }
+  },
+
+  isSameOrigin: function() {
+    var m = this.url.match(/^\s*https?:\/\/[^\/]*/);
+    return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({
+      protocol: location.protocol,
+      domain: document.domain,
+      port: location.port ? ':' + location.port : ''
+    }));
+  },
+
+  getHeader: function(name) {
+    try {
+      return this.transport.getResponseHeader(name) || null;
+    } catch (e) { return null }
+  },
+
+  evalResponse: function() {
+    try {
+      return eval((this.transport.responseText || '').unfilterJSON());
+    } catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  dispatchException: function(exception) {
+    (this.options.onException || Prototype.emptyFunction)(this, exception);
+    Ajax.Responders.dispatch('onException', this, exception);
+  }
+});
+
+Ajax.Request.Events =
+  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Response = Class.create({
+  initialize: function(request){
+    this.request = request;
+    var transport  = this.transport  = request.transport,
+        readyState = this.readyState = transport.readyState;
+
+    if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {
+      this.status       = this.getStatus();
+      this.statusText   = this.getStatusText();
+      this.responseText = String.interpret(transport.responseText);
+      this.headerJSON   = this._getHeaderJSON();
+    }
+
+    if(readyState == 4) {
+      var xml = transport.responseXML;
+      this.responseXML  = Object.isUndefined(xml) ? null : xml;
+      this.responseJSON = this._getResponseJSON();
+    }
+  },
+
+  status:      0,
+  statusText: '',
+
+  getStatus: Ajax.Request.prototype.getStatus,
+
+  getStatusText: function() {
+    try {
+      return this.transport.statusText || '';
+    } catch (e) { return '' }
+  },
+
+  getHeader: Ajax.Request.prototype.getHeader,
+
+  getAllHeaders: function() {
+    try {
+      return this.getAllResponseHeaders();
+    } catch (e) { return null }
+  },
+
+  getResponseHeader: function(name) {
+    return this.transport.getResponseHeader(name);
+  },
+
+  getAllResponseHeaders: function() {
+    return this.transport.getAllResponseHeaders();
+  },
+
+  _getHeaderJSON: function() {
+    var json = this.getHeader('X-JSON');
+    if (!json) return null;
+    json = decodeURIComponent(escape(json));
+    try {
+      return json.evalJSON(this.request.options.sanitizeJSON ||
+        !this.request.isSameOrigin());
+    } catch (e) {
+      this.request.dispatchException(e);
+    }
+  },
+
+  _getResponseJSON: function() {
+    var options = this.request.options;
+    if (!options.evalJSON || (options.evalJSON != 'force' &&
+      !(this.getHeader('Content-type') || '').include('application/json')) ||
+        this.responseText.blank())
+          return null;
+    try {
+      return this.responseText.evalJSON(options.sanitizeJSON ||
+        !this.request.isSameOrigin());
+    } catch (e) {
+      this.request.dispatchException(e);
+    }
+  }
+});
+
+Ajax.Updater = Class.create(Ajax.Request, {
+  initialize: function($super, container, url, options) {
+    this.container = {
+      success: (container.success || container),
+      failure: (container.failure || (container.success ? null : container))
+    };
+
+    options = Object.clone(options);
+    var onComplete = options.onComplete;
+    options.onComplete = (function(response, json) {
+      this.updateContent(response.responseText);
+      if (Object.isFunction(onComplete)) onComplete(response, json);
+    }).bind(this);
+
+    $super(url, options);
+  },
+
+  updateContent: function(responseText) {
+    var receiver = this.container[this.success() ? 'success' : 'failure'],
+        options = this.options;
+
+    if (!options.evalScripts) responseText = responseText.stripScripts();
+
+    if (receiver = $(receiver)) {
+      if (options.insertion) {
+        if (Object.isString(options.insertion)) {
+          var insertion = { }; insertion[options.insertion] = responseText;
+          receiver.insert(insertion);
+        }
+        else options.insertion(receiver, responseText);
+      }
+      else receiver.update(responseText);
+    }
+  }
+});
+
+Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
+  initialize: function($super, container, url, options) {
+    $super(options);
+    this.onComplete = this.options.onComplete;
+
+    this.frequency = (this.options.frequency || 2);
+    this.decay = (this.options.decay || 1);
+
+    this.updater = { };
+    this.container = container;
+    this.url = url;
+
+    this.start();
+  },
+
+  start: function() {
+    this.options.onComplete = this.updateComplete.bind(this);
+    this.onTimerEvent();
+  },
+
+  stop: function() {
+    this.updater.options.onComplete = undefined;
+    clearTimeout(this.timer);
+    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
+  },
+
+  updateComplete: function(response) {
+    if (this.options.decay) {
+      this.decay = (response.responseText == this.lastText ?
+        this.decay * this.options.decay : 1);
+
+      this.lastText = response.responseText;
+    }
+    this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);
+  },
+
+  onTimerEvent: function() {
+    this.updater = new Ajax.Updater(this.container, this.url, this.options);
+  }
+});
+function $(element) {
+  if (arguments.length > 1) {
+    for (var i = 0, elements = [], length = arguments.length; i < length; i++)
+      elements.push($(arguments[i]));
+    return elements;
+  }
+  if (Object.isString(element))
+    element = document.getElementById(element);
+  return Element.extend(element);
+}
+
+if (Prototype.BrowserFeatures.XPath) {
+  document._getElementsByXPath = function(expression, parentElement) {
+    var results = [];
+    var query = document.evaluate(expression, $(parentElement) || document,
+      null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
+    for (var i = 0, length = query.snapshotLength; i < length; i++)
+      results.push(Element.extend(query.snapshotItem(i)));
+    return results;
+  };
+}
+
+/*--------------------------------------------------------------------------*/
+
+if (!window.Node) var Node = { };
+
+if (!Node.ELEMENT_NODE) {
+  // DOM level 2 ECMAScript Language Binding
+  Object.extend(Node, {
+    ELEMENT_NODE: 1,
+    ATTRIBUTE_NODE: 2,
+    TEXT_NODE: 3,
+    CDATA_SECTION_NODE: 4,
+    ENTITY_REFERENCE_NODE: 5,
+    ENTITY_NODE: 6,
+    PROCESSING_INSTRUCTION_NODE: 7,
+    COMMENT_NODE: 8,
+    DOCUMENT_NODE: 9,
+    DOCUMENT_TYPE_NODE: 10,
+    DOCUMENT_FRAGMENT_NODE: 11,
+    NOTATION_NODE: 12
+  });
+}
+
+(function() {
+  var element = this.Element;
+  this.Element = function(tagName, attributes) {
+    attributes = attributes || { };
+    tagName = tagName.toLowerCase();
+    var cache = Element.cache;
+    if (Prototype.Browser.IE && attributes.name) {
+      tagName = '<' + tagName + ' name="' + attributes.name + '">';
+      delete attributes.name;
+      return Element.writeAttribute(document.createElement(tagName), attributes);
+    }
+    if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));
+    return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);
+  };
+  Object.extend(this.Element, element || { });
+}).call(window);
+
+Element.cache = { };
+
+Element.Methods = {
+  visible: function(element) {
+    return $(element).style.display != 'none';
+  },
+
+  toggle: function(element) {
+    element = $(element);
+    Element[Element.visible(element) ? 'hide' : 'show'](element);
+    return element;
+  },
+
+  hide: function(element) {
+    $(element).style.display = 'none';
+    return element;
+  },
+
+  show: function(element) {
+    $(element).style.display = '';
+    return element;
+  },
+
+  remove: function(element) {
+    element = $(element);
+    element.parentNode.removeChild(element);
+    return element;
+  },
+
+  update: function(element, content) {
+    element = $(element);
+    if (content && content.toElement) content = content.toElement();
+    if (Object.isElement(content)) return element.update().insert(content);
+    content = Object.toHTML(content);
+    element.innerHTML = content.stripScripts();
+    content.evalScripts.bind(content).defer();
+    return element;
+  },
+
+  replace: function(element, content) {
+    element = $(element);
+    if (content && content.toElement) content = content.toElement();
+    else if (!Object.isElement(content)) {
+      content = Object.toHTML(content);
+      var range = element.ownerDocument.createRange();
+      range.selectNode(element);
+      content.evalScripts.bind(content).defer();
+      content = range.createContextualFragment(content.stripScripts());
+    }
+    element.parentNode.replaceChild(content, element);
+    return element;
+  },
+
+  insert: function(element, insertions) {
+    element = $(element);
+
+    if (Object.isString(insertions) || Object.isNumber(insertions) ||
+        Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
+          insertions = {bottom:insertions};
+
+    var content, insert, tagName, childNodes;
+
+    for (var position in insertions) {
+      content  = insertions[position];
+      position = position.toLowerCase();
+      insert = Element._insertionTranslations[position];
+
+      if (content && content.toElement) content = content.toElement();
+      if (Object.isElement(content)) {
+        insert(element, content);
+        continue;
+      }
+
+      content = Object.toHTML(content);
+
+      tagName = ((position == 'before' || position == 'after')
+        ? element.parentNode : element).tagName.toUpperCase();
+
+      childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+
+      if (position == 'top' || position == 'after') childNodes.reverse();
+      childNodes.each(insert.curry(element));
+
+      content.evalScripts.bind(content).defer();
+    }
+
+    return element;
+  },
+
+  wrap: function(element, wrapper, attributes) {
+    element = $(element);
+    if (Object.isElement(wrapper))
+      $(wrapper).writeAttribute(attributes || { });
+    else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes);
+    else wrapper = new Element('div', wrapper);
+    if (element.parentNode)
+      element.parentNode.replaceChild(wrapper, element);
+    wrapper.appendChild(element);
+    return wrapper;
+  },
+
+  inspect: function(element) {
+    element = $(element);
+    var result = '<' + element.tagName.toLowerCase();
+    $H({'id': 'id', 'className': 'class'}).each(function(pair) {
+      var property = pair.first(), attribute = pair.last();
+      var value = (element[property] || '').toString();
+      if (value) result += ' ' + attribute + '=' + value.inspect(true);
+    });
+    return result + '>';
+  },
+
+  recursivelyCollect: function(element, property) {
+    element = $(element);
+    var elements = [];
+    while (element = element[property])
+      if (element.nodeType == 1)
+        elements.push(Element.extend(element));
+    return elements;
+  },
+
+  ancestors: function(element) {
+    return $(element).recursivelyCollect('parentNode');
+  },
+
+  descendants: function(element) {
+    return $(element).select("*");
+  },
+
+  firstDescendant: function(element) {
+    element = $(element).firstChild;
+    while (element && element.nodeType != 1) element = element.nextSibling;
+    return $(element);
+  },
+
+  immediateDescendants: function(element) {
+    if (!(element = $(element).firstChild)) return [];
+    while (element && element.nodeType != 1) element = element.nextSibling;
+    if (element) return [element].concat($(element).nextSiblings());
+    return [];
+  },
+
+  previousSiblings: function(element) {
+    return $(element).recursivelyCollect('previousSibling');
+  },
+
+  nextSiblings: function(element) {
+    return $(element).recursivelyCollect('nextSibling');
+  },
+
+  siblings: function(element) {
+    element = $(element);
+    return element.previousSiblings().reverse().concat(element.nextSiblings());
+  },
+
+  match: function(element, selector) {
+    if (Object.isString(selector))
+      selector = new Selector(selector);
+    return selector.match($(element));
+  },
+
+  up: function(element, expression, index) {
+    element = $(element);
+    if (arguments.length == 1) return $(element.parentNode);
+    var ancestors = element.ancestors();
+    return Object.isNumber(expression) ? ancestors[expression] :
+      Selector.findElement(ancestors, expression, index);
+  },
+
+  down: function(element, expression, index) {
+    element = $(element);
+    if (arguments.length == 1) return element.firstDescendant();
+    return Object.isNumber(expression) ? element.descendants()[expression] :
+      element.select(expression)[index || 0];
+  },
+
+  previous: function(element, expression, index) {
+    element = $(element);
+    if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
+    var previousSiblings = element.previousSiblings();
+    return Object.isNumber(expression) ? previousSiblings[expression] :
+      Selector.findElement(previousSiblings, expression, index);
+  },
+
+  next: function(element, expression, index) {
+    element = $(element);
+    if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
+    var nextSiblings = element.nextSiblings();
+    return Object.isNumber(expression) ? nextSiblings[expression] :
+      Selector.findElement(nextSiblings, expression, index);
+  },
+
+  select: function() {
+    var args = $A(arguments), element = $(args.shift());
+    return Selector.findChildElements(element, args);
+  },
+
+  adjacent: function() {
+    var args = $A(arguments), element = $(args.shift());
+    return Selector.findChildElements(element.parentNode, args).without(element);
+  },
+
+  identify: function(element) {
+    element = $(element);
+    var id = element.readAttribute('id'), self = arguments.callee;
+    if (id) return id;
+    do { id = 'anonymous_element_' + self.counter++ } while ($(id));
+    element.writeAttribute('id', id);
+    return id;
+  },
+
+  readAttribute: function(element, name) {
+    element = $(element);
+    if (Prototype.Browser.IE) {
+      var t = Element._attributeTranslations.read;
+      if (t.values[name]) return t.values[name](element, name);
+      if (t.names[name]) name = t.names[name];
+      if (name.include(':')) {
+        return (!element.attributes || !element.attributes[name]) ? null :
+         element.attributes[name].value;
+      }
+    }
+    return element.getAttribute(name);
+  },
+
+  writeAttribute: function(element, name, value) {
+    element = $(element);
+    var attributes = { }, t = Element._attributeTranslations.write;
+
+    if (typeof name == 'object') attributes = name;
+    else attributes[name] = Object.isUndefined(value) ? true : value;
+
+    for (var attr in attributes) {
+      name = t.names[attr] || attr;
+      value = attributes[attr];
+      if (t.values[attr]) name = t.values[attr](element, value);
+      if (value === false || value === null)
+        element.removeAttribute(name);
+      else if (value === true)
+        element.setAttribute(name, name);
+      else element.setAttribute(name, value);
+    }
+    return element;
+  },
+
+  getHeight: function(element) {
+    return $(element).getDimensions().height;
+  },
+
+  getWidth: function(element) {
+    return $(element).getDimensions().width;
+  },
+
+  classNames: function(element) {
+    return new Element.ClassNames(element);
+  },
+
+  hasClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    var elementClassName = element.className;
+    return (elementClassName.length > 0 && (elementClassName == className ||
+      new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
+  },
+
+  addClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    if (!element.hasClassName(className))
+      element.className += (element.className ? ' ' : '') + className;
+    return element;
+  },
+
+  removeClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    element.className = element.className.replace(
+      new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();
+    return element;
+  },
+
+  toggleClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    return element[element.hasClassName(className) ?
+      'removeClassName' : 'addClassName'](className);
+  },
+
+  // removes whitespace-only text node children
+  cleanWhitespace: function(element) {
+    element = $(element);
+    var node = element.firstChild;
+    while (node) {
+      var nextNode = node.nextSibling;
+      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
+        element.removeChild(node);
+      node = nextNode;
+    }
+    return element;
+  },
+
+  empty: function(element) {
+    return $(element).innerHTML.blank();
+  },
+
+  descendantOf: function(element, ancestor) {
+    element = $(element), ancestor = $(ancestor);
+    var originalAncestor = ancestor;
+
+    if (element.compareDocumentPosition)
+      return (element.compareDocumentPosition(ancestor) & 8) === 8;
+
+    if (element.sourceIndex && !Prototype.Browser.Opera) {
+      var e = element.sourceIndex, a = ancestor.sourceIndex,
+       nextAncestor = ancestor.nextSibling;
+      if (!nextAncestor) {
+        do { ancestor = ancestor.parentNode; }
+        while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode);
+      }
+      if (nextAncestor && nextAncestor.sourceIndex)
+       return (e > a && e < nextAncestor.sourceIndex);
+    }
+
+    while (element = element.parentNode)
+      if (element == originalAncestor) return true;
+    return false;
+  },
+
+  scrollTo: function(element) {
+    element = $(element);
+    var pos = element.cumulativeOffset();
+    window.scrollTo(pos[0], pos[1]);
+    return element;
+  },
+
+  getStyle: function(element, style) {
+    element = $(element);
+    style = style == 'float' ? 'cssFloat' : style.camelize();
+    var value = element.style[style];
+    if (!value) {
+      var css = document.defaultView.getComputedStyle(element, null);
+      value = css ? css[style] : null;
+    }
+    if (style == 'opacity') return value ? parseFloat(value) : 1.0;
+    return value == 'auto' ? null : value;
+  },
+
+  getOpacity: function(element) {
+    return $(element).getStyle('opacity');
+  },
+
+  setStyle: function(element, styles) {
+    element = $(element);
+    var elementStyle = element.style, match;
+    if (Object.isString(styles)) {
+      element.style.cssText += ';' + styles;
+      return styles.include('opacity') ?
+        element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element;
+    }
+    for (var property in styles)
+      if (property == 'opacity') element.setOpacity(styles[property]);
+      else
+        elementStyle[(property == 'float' || property == 'cssFloat') ?
+          (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') :
+            property] = styles[property];
+
+    return element;
+  },
+
+  setOpacity: function(element, value) {
+    element = $(element);
+    element.style.opacity = (value == 1 || value === '') ? '' :
+      (value < 0.00001) ? 0 : value;
+    return element;
+  },
+
+  getDimensions: function(element) {
+    element = $(element);
+    var display = $(element).getStyle('display');
+    if (display != 'none' && display != null) // Safari bug
+      return {width: element.offsetWidth, height: element.offsetHeight};
+
+    // All *Width and *Height properties give 0 on elements with display none,
+    // so enable the element temporarily
+    var els = element.style;
+    var originalVisibility = els.visibility;
+    var originalPosition = els.position;
+    var originalDisplay = els.display;
+    els.visibility = 'hidden';
+    els.position = 'absolute';
+    els.display = 'block';
+    var originalWidth = element.clientWidth;
+    var originalHeight = element.clientHeight;
+    els.display = originalDisplay;
+    els.position = originalPosition;
+    els.visibility = originalVisibility;
+    return {width: originalWidth, height: originalHeight};
+  },
+
+  makePositioned: function(element) {
+    element = $(element);
+    var pos = Element.getStyle(element, 'position');
+    if (pos == 'static' || !pos) {
+      element._madePositioned = true;
+      element.style.position = 'relative';
+      // Opera returns the offset relative to the positioning context, when an
+      // element is position relative but top and left have not been defined
+      if (window.opera) {
+        element.style.top = 0;
+        element.style.left = 0;
+      }
+    }
+    return element;
+  },
+
+  undoPositioned: function(element) {
+    element = $(element);
+    if (element._madePositioned) {
+      element._madePositioned = undefined;
+      element.style.position =
+        element.style.top =
+        element.style.left =
+        element.style.bottom =
+        element.style.right = '';
+    }
+    return element;
+  },
+
+  makeClipping: function(element) {
+    element = $(element);
+    if (element._overflow) return element;
+    element._overflow = Element.getStyle(element, 'overflow') || 'auto';
+    if (element._overflow !== 'hidden')
+      element.style.overflow = 'hidden';
+    return element;
+  },
+
+  undoClipping: function(element) {
+    element = $(element);
+    if (!element._overflow) return element;
+    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
+    element._overflow = null;
+    return element;
+  },
+
+  cumulativeOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+    } while (element);
+    return Element._returnOffset(valueL, valueT);
+  },
+
+  positionedOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+      if (element) {
+        if (element.tagName == 'BODY') break;
+        var p = Element.getStyle(element, 'position');
+        if (p !== 'static') break;
+      }
+    } while (element);
+    return Element._returnOffset(valueL, valueT);
+  },
+
+  absolutize: function(element) {
+    element = $(element);
+    if (element.getStyle('position') == 'absolute') return;
+    // Position.prepare(); // To be done manually by Scripty when it needs it.
+
+    var offsets = element.positionedOffset();
+    var top     = offsets[1];
+    var left    = offsets[0];
+    var width   = element.clientWidth;
+    var height  = element.clientHeight;
+
+    element._originalLeft   = left - parseFloat(element.style.left  || 0);
+    element._originalTop    = top  - parseFloat(element.style.top || 0);
+    element._originalWidth  = element.style.width;
+    element._originalHeight = element.style.height;
+
+    element.style.position = 'absolute';
+    element.style.top    = top + 'px';
+    element.style.left   = left + 'px';
+    element.style.width  = width + 'px';
+    element.style.height = height + 'px';
+    return element;
+  },
+
+  relativize: function(element) {
+    element = $(element);
+    if (element.getStyle('position') == 'relative') return;
+    // Position.prepare(); // To be done manually by Scripty when it needs it.
+
+    element.style.position = 'relative';
+    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
+    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+
+    element.style.top    = top + 'px';
+    element.style.left   = left + 'px';
+    element.style.height = element._originalHeight;
+    element.style.width  = element._originalWidth;
+    return element;
+  },
+
+  cumulativeScrollOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.scrollTop  || 0;
+      valueL += element.scrollLeft || 0;
+      element = element.parentNode;
+    } while (element);
+    return Element._returnOffset(valueL, valueT);
+  },
+
+  getOffsetParent: function(element) {
+    element = $(element);  
+    var op = element.offsetParent, body = document.body, docEl = document.documentElement;  
+
+    /* IE with strict doctype may try to return documentElement as offsetParent  
+       on relatively positioned elements, we will return body instead */  
+    if (op && op !== docEl) return $(op);  
+    if (op === docEl || element === docEl || element === body) return $(body);  
+         
+    while ((element = element.parentNode) && element !== body)  
+      if (Element.getStyle(element, 'position') != 'static')
+        return $(element);
+
+    return $(body);
+  },
+
+  viewportOffset: function(forElement) {
+    forElement = $(forElement);
+
+    var element = forElement, valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+    } while ((element = element.getOffsetParent()) != document.body);
+
+    element = forElement;
+    do {
+      if (!Prototype.Browser.Opera || element.tagName == 'BODY') {
+        valueT -= element.scrollTop  || 0;
+        valueL -= element.scrollLeft || 0;
+      }
+    } while (element = element.parentNode);
+
+    return Element._returnOffset(valueL, valueT);
+  },
+
+  clonePosition: function(element, source) {
+    var options = Object.extend({
+      setLeft:    true,
+      setTop:     true,
+      setWidth:   true,
+      setHeight:  true,
+      offsetTop:  0,
+      offsetLeft: 0
+    }, arguments[2] || { });
+
+    // find page position of source
+    source = $(source);
+    var p = source.viewportOffset();
+
+    // find coordinate system to use
+    element = $(element);
+    var delta = [0, 0];
+    var parent = null;
+    // delta [0,0] will do fine with position: fixed elements,
+    // position:absolute needs offsetParent deltas
+    if (Element.getStyle(element, 'position') == 'absolute') {
+      parent = element.getOffsetParent();
+      delta = parent.viewportOffset();
+    }
+
+    // correct by body offsets (fixes Safari)
+    if (parent == document.body) {
+      delta[0] -= document.body.offsetLeft;
+      delta[1] -= document.body.offsetTop;
+    }
+
+    // set position
+    if (options.setLeft)   element.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
+    if (options.setTop)    element.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
+    if (options.setWidth)  element.style.width = source.offsetWidth + 'px';
+    if (options.setHeight) element.style.height = source.offsetHeight + 'px';
+    return element;
+  }
+};
+
+Element.Methods.identify.counter = 1;
+
+Object.extend(Element.Methods, {
+  getElementsBySelector: Element.Methods.select,
+  childElements: Element.Methods.immediateDescendants
+});
+
+Element._attributeTranslations = {
+  write: {
+    names: {
+      className: 'class',
+      htmlFor:   'for'
+    },
+    values: { }
+  }
+};
+
+if (Prototype.Browser.Opera) {
+  Element.Methods.getStyle = Element.Methods.getStyle.wrap(
+    function(proceed, element, style) {
+      switch (style) {
+        case 'left': case 'top': case 'right': case 'bottom':
+          if (proceed(element, 'position') === 'static') return null;
+        case 'height': case 'width':
+          // returns '0px' for hidden elements; we want it to return null
+          if (!Element.visible(element)) return null;
+
+          // returns the border-box dimensions rather than the content-box
+          // dimensions, so we subtract padding and borders from the value
+          var dim = parseInt(proceed(element, style), 10);
+
+          if (dim !== element['offset' + style.capitalize()])
+            return dim + 'px';
+
+          var properties;
+          if (style === 'height') {
+            properties = ['border-top-width', 'padding-top',
+             'padding-bottom', 'border-bottom-width'];
+          }
+          else {
+            properties = ['border-left-width', 'padding-left',
+             'padding-right', 'border-right-width'];
+          }
+          return properties.inject(dim, function(memo, property) {
+            var val = proceed(element, property);
+            return val === null ? memo : memo - parseInt(val, 10);
+          }) + 'px';
+        default: return proceed(element, style);
+      }
+    }
+  );
+
+  Element.Methods.readAttribute = Element.Methods.readAttribute.wrap(
+    function(proceed, element, attribute) {
+      if (attribute === 'title') return element.title;
+      return proceed(element, attribute);
+    }
+  );
+}
+
+else if (Prototype.Browser.IE) {
+  // IE doesn't report offsets correctly for static elements, so we change them
+  // to "relative" to get the values, then change them back.
+  Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap(
+    function(proceed, element) {
+      element = $(element);
+      var position = element.getStyle('position');
+      if (position !== 'static') return proceed(element);
+      element.setStyle({ position: 'relative' });
+      var value = proceed(element);
+      element.setStyle({ position: position });
+      return value;
+    }
+  );
+
+  $w('positionedOffset viewportOffset').each(function(method) {
+    Element.Methods[method] = Element.Methods[method].wrap(
+      function(proceed, element) {
+        element = $(element);
+        var position = element.getStyle('position');
+        if (position !== 'static') return proceed(element);
+        // Trigger hasLayout on the offset parent so that IE6 reports
+        // accurate offsetTop and offsetLeft values for position: fixed.
+        var offsetParent = element.getOffsetParent();
+        if (offsetParent && offsetParent.getStyle('position') === 'fixed')
+          offsetParent.setStyle({ zoom: 1 });
+        element.setStyle({ position: 'relative' });
+        var value = proceed(element);
+        element.setStyle({ position: position });
+        return value;
+      }
+    );
+  });
+
+  Element.Methods.getStyle = function(element, style) {
+    element = $(element);
+    style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
+    var value = element.style[style];
+    if (!value && element.currentStyle) value = element.currentStyle[style];
+
+    if (style == 'opacity') {
+      if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
+        if (value[1]) return parseFloat(value[1]) / 100;
+      return 1.0;
+    }
+
+    if (value == 'auto') {
+      if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
+        return element['offset' + style.capitalize()] + 'px';
+      return null;
+    }
+    return value;
+  };
+
+  Element.Methods.setOpacity = function(element, value) {
+    function stripAlpha(filter){
+      return filter.replace(/alpha\([^\)]*\)/gi,'');
+    }
+    element = $(element);
+    var currentStyle = element.currentStyle;
+    if ((currentStyle && !currentStyle.hasLayout) ||
+      (!currentStyle && element.style.zoom == 'normal'))
+        element.style.zoom = 1;
+
+    var filter = element.getStyle('filter'), style = element.style;
+    if (value == 1 || value === '') {
+      (filter = stripAlpha(filter)) ?
+        style.filter = filter : style.removeAttribute('filter');
+      return element;
+    } else if (value < 0.00001) value = 0;
+    style.filter = stripAlpha(filter) +
+      'alpha(opacity=' + (value * 100) + ')';
+    return element;
+  };
+
+  Element._attributeTranslations = {
+    read: {
+      names: {
+        'class': 'className',
+        'for':   'htmlFor'
+      },
+      values: {
+        _getAttr: function(element, attribute) {
+          return element.getAttribute(attribute, 2);
+        },
+        _getAttrNode: function(element, attribute) {
+          var node = element.getAttributeNode(attribute);
+          return node ? node.value : "";
+        },
+        _getEv: function(element, attribute) {
+          attribute = element.getAttribute(attribute);
+          return attribute ? attribute.toString().slice(23, -2) : null;
+        },
+        _flag: function(element, attribute) {
+          return $(element).hasAttribute(attribute) ? attribute : null;
+        },
+        style: function(element) {
+          return element.style.cssText.toLowerCase();
+        },
+        title: function(element) {
+          return element.title;
+        }
+      }
+    }
+  };
+
+  Element._attributeTranslations.write = {
+    names: Object.extend({
+      cellpadding: 'cellPadding',
+      cellspacing: 'cellSpacing'
+    }, Element._attributeTranslations.read.names),
+    values: {
+      checked: function(element, value) {
+        element.checked = !!value;
+      },
+
+      style: function(element, value) {
+        element.style.cssText = value ? value : '';
+      }
+    }
+  };
+
+  Element._attributeTranslations.has = {};
+
+  $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' +
+      'encType maxLength readOnly longDesc').each(function(attr) {
+    Element._attributeTranslations.write.names[attr.toLowerCase()] = attr;
+    Element._attributeTranslations.has[attr.toLowerCase()] = attr;
+  });
+
+  (function(v) {
+    Object.extend(v, {
+      href:        v._getAttr,
+      src:         v._getAttr,
+      type:        v._getAttr,
+      action:      v._getAttrNode,
+      disabled:    v._flag,
+      checked:     v._flag,
+      readonly:    v._flag,
+      multiple:    v._flag,
+      onload:      v._getEv,
+      onunload:    v._getEv,
+      onclick:     v._getEv,
+      ondblclick:  v._getEv,
+      onmousedown: v._getEv,
+      onmouseup:   v._getEv,
+      onmouseover: v._getEv,
+      onmousemove: v._getEv,
+      onmouseout:  v._getEv,
+      onfocus:     v._getEv,
+      onblur:      v._getEv,
+      onkeypress:  v._getEv,
+      onkeydown:   v._getEv,
+      onkeyup:     v._getEv,
+      onsubmit:    v._getEv,
+      onreset:     v._getEv,
+      onselect:    v._getEv,
+      onchange:    v._getEv
+    });
+  })(Element._attributeTranslations.read.values);
+}
+
+else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) {
+  Element.Methods.setOpacity = function(element, value) {
+    element = $(element);
+    element.style.opacity = (value == 1) ? 0.999999 :
+      (value === '') ? '' : (value < 0.00001) ? 0 : value;
+    return element;
+  };
+}
+
+else if (Prototype.Browser.WebKit) {
+  Element.Methods.setOpacity = function(element, value) {
+    element = $(element);
+    element.style.opacity = (value == 1 || value === '') ? '' :
+      (value < 0.00001) ? 0 : value;
+
+    if (value == 1)
+      if(element.tagName == 'IMG' && element.width) {
+        element.width++; element.width--;
+      } else try {
+        var n = document.createTextNode(' ');
+        element.appendChild(n);
+        element.removeChild(n);
+      } catch (e) { }
+
+    return element;
+  };
+
+  // Safari returns margins on body which is incorrect if the child is absolutely
+  // positioned.  For performance reasons, redefine Element#cumulativeOffset for
+  // KHTML/WebKit only.
+  Element.Methods.cumulativeOffset = function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      if (element.offsetParent == document.body)
+        if (Element.getStyle(element, 'position') == 'absolute') break;
+
+      element = element.offsetParent;
+    } while (element);
+
+    return Element._returnOffset(valueL, valueT);
+  };
+}
+
+if (Prototype.Browser.IE || Prototype.Browser.Opera) {
+  // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements
+  Element.Methods.update = function(element, content) {
+    element = $(element);
+
+    if (content && content.toElement) content = content.toElement();
+    if (Object.isElement(content)) return element.update().insert(content);
+
+    content = Object.toHTML(content);
+    var tagName = element.tagName.toUpperCase();
+
+    if (tagName in Element._insertionTranslations.tags) {
+      $A(element.childNodes).each(function(node) { element.removeChild(node) });
+      Element._getContentFromAnonymousElement(tagName, content.stripScripts())
+        .each(function(node) { element.appendChild(node) });
+    }
+    else element.innerHTML = content.stripScripts();
+
+    content.evalScripts.bind(content).defer();
+    return element;
+  };
+}
+
+if ('outerHTML' in document.createElement('div')) {
+  Element.Methods.replace = function(element, content) {
+    element = $(element);
+
+    if (content && content.toElement) content = content.toElement();
+    if (Object.isElement(content)) {
+      element.parentNode.replaceChild(content, element);
+      return element;
+    }
+
+    content = Object.toHTML(content);
+    var parent = element.parentNode, tagName = parent.tagName.toUpperCase();
+
+    if (Element._insertionTranslations.tags[tagName]) {
+      var nextSibling = element.next();
+      var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+      parent.removeChild(element);
+      if (nextSibling)
+        fragments.each(function(node) { parent.insertBefore(node, nextSibling) });
+      else
+        fragments.each(function(node) { parent.appendChild(node) });
+    }
+    else element.outerHTML = content.stripScripts();
+
+    content.evalScripts.bind(content).defer();
+    return element;
+  };
+}
+
+Element._returnOffset = function(l, t) {
+  var result = [l, t];
+  result.left = l;
+  result.top = t;
+  return result;
+};
+
+Element._getContentFromAnonymousElement = function(tagName, html) {
+  var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
+  if (t) {
+    div.innerHTML = t[0] + html + t[1];
+    t[2].times(function() { div = div.firstChild });
+  } else div.innerHTML = html;
+  return $A(div.childNodes);
+};
+
+Element._insertionTranslations = {
+  before: function(element, node) {
+    element.parentNode.insertBefore(node, element);
+  },
+  top: function(element, node) {
+    element.insertBefore(node, element.firstChild);
+  },
+  bottom: function(element, node) {
+    element.appendChild(node);
+  },
+  after: function(element, node) {
+    element.parentNode.insertBefore(node, element.nextSibling);
+  },
+  tags: {
+    TABLE:  ['<table>',                '</table>',                   1],
+    TBODY:  ['<table><tbody>',         '</tbody></table>',           2],
+    TR:     ['<table><tbody><tr>',     '</tr></tbody></table>',      3],
+    TD:     ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4],
+    SELECT: ['<select>',               '</select>',                  1]
+  }
+};
+
+(function() {
+  Object.extend(this.tags, {
+    THEAD: this.tags.TBODY,
+    TFOOT: this.tags.TBODY,
+    TH:    this.tags.TD
+  });
+}).call(Element._insertionTranslations);
+
+Element.Methods.Simulated = {
+  hasAttribute: function(element, attribute) {
+    attribute = Element._attributeTranslations.has[attribute] || attribute;
+    var node = $(element).getAttributeNode(attribute);
+    return node && node.specified;
+  }
+};
+
+Element.Methods.ByTag = { };
+
+Object.extend(Element, Element.Methods);
+
+if (!Prototype.BrowserFeatures.ElementExtensions &&
+    document.createElement('div').__proto__) {
+  window.HTMLElement = { };
+  window.HTMLElement.prototype = document.createElement('div').__proto__;
+  Prototype.BrowserFeatures.ElementExtensions = true;
+}
+
+Element.extend = (function() {
+  if (Prototype.BrowserFeatures.SpecificElementExtensions)
+    return Prototype.K;
+
+  var Methods = { }, ByTag = Element.Methods.ByTag;
+
+  var extend = Object.extend(function(element) {
+    if (!element || element._extendedByPrototype ||
+        element.nodeType != 1 || element == window) return element;
+
+    var methods = Object.clone(Methods),
+      tagName = element.tagName, property, value;
+
+    // extend methods for specific tags
+    if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]);
+
+    for (property in methods) {
+      value = methods[property];
+      if (Object.isFunction(value) && !(property in element))
+        element[property] = value.methodize();
+    }
+
+    element._extendedByPrototype = Prototype.emptyFunction;
+    return element;
+
+  }, {
+    refresh: function() {
+      // extend methods for all tags (Safari doesn't need this)
+      if (!Prototype.BrowserFeatures.ElementExtensions) {
+        Object.extend(Methods, Element.Methods);
+        Object.extend(Methods, Element.Methods.Simulated);
+      }
+    }
+  });
+
+  extend.refresh();
+  return extend;
+})();
+
+Element.hasAttribute = function(element, attribute) {
+  if (element.hasAttribute) return element.hasAttribute(attribute);
+  return Element.Methods.Simulated.hasAttribute(element, attribute);
+};
+
+Element.addMethods = function(methods) {
+  var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;
+
+  if (!methods) {
+    Object.extend(Form, Form.Methods);
+    Object.extend(Form.Element, Form.Element.Methods);
+    Object.extend(Element.Methods.ByTag, {
+      "FORM":     Object.clone(Form.Methods),
+      "INPUT":    Object.clone(Form.Element.Methods),
+      "SELECT":   Object.clone(Form.Element.Methods),
+      "TEXTAREA": Object.clone(Form.Element.Methods)
+    });
+  }
+
+  if (arguments.length == 2) {
+    var tagName = methods;
+    methods = arguments[1];
+  }
+
+  if (!tagName) Object.extend(Element.Methods, methods || { });
+  else {
+    if (Object.isArray(tagName)) tagName.each(extend);
+    else extend(tagName);
+  }
+
+  function extend(tagName) {
+    tagName = tagName.toUpperCase();
+    if (!Element.Methods.ByTag[tagName])
+      Element.Methods.ByTag[tagName] = { };
+    Object.extend(Element.Methods.ByTag[tagName], methods);
+  }
+
+  function copy(methods, destination, onlyIfAbsent) {
+    onlyIfAbsent = onlyIfAbsent || false;
+    for (var property in methods) {
+      var value = methods[property];
+      if (!Object.isFunction(value)) continue;
+      if (!onlyIfAbsent || !(property in destination))
+        destination[property] = value.methodize();
+    }
+  }
+
+  function findDOMClass(tagName) {
+    var klass;
+    var trans = {
+      "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
+      "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
+      "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
+      "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
+      "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
+      "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
+      "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
+      "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
+      "FrameSet", "IFRAME": "IFrame"
+    };
+    if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
+    if (window[klass]) return window[klass];
+    klass = 'HTML' + tagName + 'Element';
+    if (window[klass]) return window[klass];
+    klass = 'HTML' + tagName.capitalize() + 'Element';
+    if (window[klass]) return window[klass];
+
+    window[klass] = { };
+    window[klass].prototype = document.createElement(tagName).__proto__;
+    return window[klass];
+  }
+
+  if (F.ElementExtensions) {
+    copy(Element.Methods, HTMLElement.prototype);
+    copy(Element.Methods.Simulated, HTMLElement.prototype, true);
+  }
+
+  if (F.SpecificElementExtensions) {
+    for (var tag in Element.Methods.ByTag) {
+      var klass = findDOMClass(tag);
+      if (Object.isUndefined(klass)) continue;
+      copy(T[tag], klass.prototype);
+    }
+  }
+
+  Object.extend(Element, Element.Methods);
+  delete Element.ByTag;
+
+  if (Element.extend.refresh) Element.extend.refresh();
+  Element.cache = { };
+};
+
+document.viewport = {
+  getDimensions: function() {
+    var dimensions = { };
+    var B = Prototype.Browser;
+    $w('width height').each(function(d) {
+      var D = d.capitalize();
+      dimensions[d] = (B.WebKit && !document.evaluate) ? self['inner' + D] :
+        (B.Opera) ? document.body['client' + D] : document.documentElement['client' + D];
+    });
+    return dimensions;
+  },
+
+  getWidth: function() {
+    return this.getDimensions().width;
+  },
+
+  getHeight: function() {
+    return this.getDimensions().height;
+  },
+
+  getScrollOffsets: function() {
+    return Element._returnOffset(
+      window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
+      window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
+  }
+};
+/* Portions of the Selector class are derived from Jack Slocum’s DomQuery,
+ * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
+ * license.  Please see http://www.yui-ext.com/ for more information. */
+
+var Selector = Class.create({
+  initialize: function(expression) {
+    this.expression = expression.strip();
+    this.compileMatcher();
+  },
+
+  shouldUseXPath: function() {
+    if (!Prototype.BrowserFeatures.XPath) return false;
+
+    var e = this.expression;
+
+    // Safari 3 chokes on :*-of-type and :empty
+    if (Prototype.Browser.WebKit &&
+     (e.include("-of-type") || e.include(":empty")))
+      return false;
+
+    // XPath can't do namespaced attributes, nor can it read
+    // the "checked" property from DOM nodes
+    if ((/(\[[\w-]*?:|:checked)/).test(this.expression))
+      return false;
+
+    return true;
+  },
+
+  compileMatcher: function() {
+    if (this.shouldUseXPath())
+      return this.compileXPathMatcher();
+
+    var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
+        c = Selector.criteria, le, p, m;
+
+    if (Selector._cache[e]) {
+      this.matcher = Selector._cache[e];
+      return;
+    }
+
+    this.matcher = ["this.matcher = function(root) {",
+                    "var r = root, h = Selector.handlers, c = false, n;"];
+
+    while (e && le != e && (/\S/).test(e)) {
+      le = e;
+      for (var i in ps) {
+        p = ps[i];
+        if (m = e.match(p)) {
+          this.matcher.push(Object.isFunction(c[i]) ? c[i](m) :
+             new Template(c[i]).evaluate(m));
+          e = e.replace(m[0], '');
+          break;
+        }
+      }
+    }
+
+    this.matcher.push("return h.unique(n);\n}");
+    eval(this.matcher.join('\n'));
+    Selector._cache[this.expression] = this.matcher;
+  },
+
+  compileXPathMatcher: function() {
+    var e = this.expression, ps = Selector.patterns,
+        x = Selector.xpath, le, m;
+
+    if (Selector._cache[e]) {
+      this.xpath = Selector._cache[e]; return;
+    }
+
+    this.matcher = ['.//*'];
+    while (e && le != e && (/\S/).test(e)) {
+      le = e;
+      for (var i in ps) {
+        if (m = e.match(ps[i])) {
+          this.matcher.push(Object.isFunction(x[i]) ? x[i](m) :
+            new Template(x[i]).evaluate(m));
+          e = e.replace(m[0], '');
+          break;
+        }
+      }
+    }
+
+    this.xpath = this.matcher.join('');
+    Selector._cache[this.expression] = this.xpath;
+  },
+
+  findElements: function(root) {
+    root = root || document;
+    if (this.xpath) return document._getElementsByXPath(this.xpath, root);
+    return this.matcher(root);
+  },
+
+  match: function(element) {
+    this.tokens = [];
+
+    var e = this.expression, ps = Selector.patterns, as = Selector.assertions;
+    var le, p, m;
+
+    while (e && le !== e && (/\S/).test(e)) {
+      le = e;
+      for (var i in ps) {
+        p = ps[i];
+        if (m = e.match(p)) {
+          // use the Selector.assertions methods unless the selector
+          // is too complex.
+          if (as[i]) {
+            this.tokens.push([i, Object.clone(m)]);
+            e = e.replace(m[0], '');
+          } else {
+            // reluctantly do a document-wide search
+            // and look for a match in the array
+            return this.findElements(document).include(element);
+          }
+        }
+      }
+    }
+
+    var match = true, name, matches;
+    for (var i = 0, token; token = this.tokens[i]; i++) {
+      name = token[0], matches = token[1];
+      if (!Selector.assertions[name](element, matches)) {
+        match = false; break;
+      }
+    }
+
+    return match;
+  },
+
+  toString: function() {
+    return this.expression;
+  },
+
+  inspect: function() {
+    return "#<Selector:" + this.expression.inspect() + ">";
+  }
+});
+
+Object.extend(Selector, {
+  _cache: { },
+
+  xpath: {
+    descendant:   "//*",
+    child:        "/*",
+    adjacent:     "/following-sibling::*[1]",
+    laterSibling: '/following-sibling::*',
+    tagName:      function(m) {
+      if (m[1] == '*') return '';
+      return "[local-name()='" + m[1].toLowerCase() +
+             "' or local-name()='" + m[1].toUpperCase() + "']";
+    },
+    className:    "[contains(concat(' ', @class, ' '), ' #{1} ')]",
+    id:           "[@id='#{1}']",
+    attrPresence: function(m) {
+      m[1] = m[1].toLowerCase();
+      return new Template("[@#{1}]").evaluate(m);
+    },
+    attr: function(m) {
+      m[1] = m[1].toLowerCase();
+      m[3] = m[5] || m[6];
+      return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
+    },
+    pseudo: function(m) {
+      var h = Selector.xpath.pseudos[m[1]];
+      if (!h) return '';
+      if (Object.isFunction(h)) return h(m);
+      return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
+    },
+    operators: {
+      '=':  "[@#{1}='#{3}']",
+      '!=': "[@#{1}!='#{3}']",
+      '^=': "[starts-with(@#{1}, '#{3}')]",
+      '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
+      '*=': "[contains(@#{1}, '#{3}')]",
+      '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
+      '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
+    },
+    pseudos: {
+      'first-child': '[not(preceding-sibling::*)]',
+      'last-child':  '[not(following-sibling::*)]',
+      'only-child':  '[not(preceding-sibling::* or following-sibling::*)]',
+      'empty':       "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]",
+      'checked':     "[@checked]",
+      'disabled':    "[@disabled]",
+      'enabled':     "[not(@disabled)]",
+      'not': function(m) {
+        var e = m[6], p = Selector.patterns,
+            x = Selector.xpath, le, v;
+
+        var exclusion = [];
+        while (e && le != e && (/\S/).test(e)) {
+          le = e;
+          for (var i in p) {
+            if (m = e.match(p[i])) {
+              v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m);
+              exclusion.push("(" + v.substring(1, v.length - 1) + ")");
+              e = e.replace(m[0], '');
+              break;
+            }
+          }
+        }
+        return "[not(" + exclusion.join(" and ") + ")]";
+      },
+      'nth-child':      function(m) {
+        return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
+      },
+      'nth-last-child': function(m) {
+        return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
+      },
+      'nth-of-type':    function(m) {
+        return Selector.xpath.pseudos.nth("position() ", m);
+      },
+      'nth-last-of-type': function(m) {
+        return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
+      },
+      'first-of-type':  function(m) {
+        m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
+      },
+      'last-of-type':   function(m) {
+        m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
+      },
+      'only-of-type':   function(m) {
+        var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
+      },
+      nth: function(fragment, m) {
+        var mm, formula = m[6], predicate;
+        if (formula == 'even') formula = '2n+0';
+        if (formula == 'odd')  formula = '2n+1';
+        if (mm = formula.match(/^(\d+)$/)) // digit only
+          return '[' + fragment + "= " + mm[1] + ']';
+        if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+          if (mm[1] == "-") mm[1] = -1;
+          var a = mm[1] ? Number(mm[1]) : 1;
+          var b = mm[2] ? Number(mm[2]) : 0;
+          predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
+          "((#{fragment} - #{b}) div #{a} >= 0)]";
+          return new Template(predicate).evaluate({
+            fragment: fragment, a: a, b: b });
+        }
+      }
+    }
+  },
+
+  criteria: {
+    tagName:      'n = h.tagName(n, r, "#{1}", c);      c = false;',
+    className:    'n = h.className(n, r, "#{1}", c);    c = false;',
+    id:           'n = h.id(n, r, "#{1}", c);           c = false;',
+    attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;',
+    attr: function(m) {
+      m[3] = (m[5] || m[6]);
+      return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m);
+    },
+    pseudo: function(m) {
+      if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
+      return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
+    },
+    descendant:   'c = "descendant";',
+    child:        'c = "child";',
+    adjacent:     'c = "adjacent";',
+    laterSibling: 'c = "laterSibling";'
+  },
+
+  patterns: {
+    // combinators must be listed first
+    // (and descendant needs to be last combinator)
+    laterSibling: /^\s*~\s*/,
+    child:        /^\s*>\s*/,
+    adjacent:     /^\s*\+\s*/,
+    descendant:   /^\s/,
+
+    // selectors follow
+    tagName:      /^\s*(\*|[\w\-]+)(\b|$)?/,
+    id:           /^#([\w\-\*]+)(\b|$)/,
+    className:    /^\.([\w\-\*]+)(\b|$)/,
+    pseudo:
+/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/,
+    attrPresence: /^\[([\w]+)\]/,
+    attr:         /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/
+  },
+
+  // for Selector.match and Element#match
+  assertions: {
+    tagName: function(element, matches) {
+      return matches[1].toUpperCase() == element.tagName.toUpperCase();
+    },
+
+    className: function(element, matches) {
+      return Element.hasClassName(element, matches[1]);
+    },
+
+    id: function(element, matches) {
+      return element.id === matches[1];
+    },
+
+    attrPresence: function(element, matches) {
+      return Element.hasAttribute(element, matches[1]);
+    },
+
+    attr: function(element, matches) {
+      var nodeValue = Element.readAttribute(element, matches[1]);
+      return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]);
+    }
+  },
+
+  handlers: {
+    // UTILITY FUNCTIONS
+    // joins two collections
+    concat: function(a, b) {
+      for (var i = 0, node; node = b[i]; i++)
+        a.push(node);
+      return a;
+    },
+
+    // marks an array of nodes for counting
+    mark: function(nodes) {
+      var _true = Prototype.emptyFunction;
+      for (var i = 0, node; node = nodes[i]; i++)
+        node._countedByPrototype = _true;
+      return nodes;
+    },
+
+    unmark: function(nodes) {
+      for (var i = 0, node; node = nodes[i]; i++)
+        node._countedByPrototype = undefined;
+      return nodes;
+    },
+
+    // mark each child node with its position (for nth calls)
+    // "ofType" flag indicates whether we're indexing for nth-of-type
+    // rather than nth-child
+    index: function(parentNode, reverse, ofType) {
+      parentNode._countedByPrototype = Prototype.emptyFunction;
+      if (reverse) {
+        for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
+          var node = nodes[i];
+          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
+        }
+      } else {
+        for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
+          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
+      }
+    },
+
+    // filters out duplicates and extends all nodes
+    unique: function(nodes) {
+      if (nodes.length == 0) return nodes;
+      var results = [], n;
+      for (var i = 0, l = nodes.length; i < l; i++)
+        if (!(n = nodes[i])._countedByPrototype) {
+          n._countedByPrototype = Prototype.emptyFunction;
+          results.push(Element.extend(n));
+        }
+      return Selector.handlers.unmark(results);
+    },
+
+    // COMBINATOR FUNCTIONS
+    descendant: function(nodes) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        h.concat(results, node.getElementsByTagName('*'));
+      return results;
+    },
+
+    child: function(nodes) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        for (var j = 0, child; child = node.childNodes[j]; j++)
+          if (child.nodeType == 1 && child.tagName != '!') results.push(child);
+      }
+      return results;
+    },
+
+    adjacent: function(nodes) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        var next = this.nextElementSibling(node);
+        if (next) results.push(next);
+      }
+      return results;
+    },
+
+    laterSibling: function(nodes) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        h.concat(results, Element.nextSiblings(node));
+      return results;
+    },
+
+    nextElementSibling: function(node) {
+      while (node = node.nextSibling)
+             if (node.nodeType == 1) return node;
+      return null;
+    },
+
+    previousElementSibling: function(node) {
+      while (node = node.previousSibling)
+        if (node.nodeType == 1) return node;
+      return null;
+    },
+
+    // TOKEN FUNCTIONS
+    tagName: function(nodes, root, tagName, combinator) {
+      var uTagName = tagName.toUpperCase();
+      var results = [], h = Selector.handlers;
+      if (nodes) {
+        if (combinator) {
+          // fastlane for ordinary descendant combinators
+          if (combinator == "descendant") {
+            for (var i = 0, node; node = nodes[i]; i++)
+              h.concat(results, node.getElementsByTagName(tagName));
+            return results;
+          } else nodes = this[combinator](nodes);
+          if (tagName == "*") return nodes;
+        }
+        for (var i = 0, node; node = nodes[i]; i++)
+          if (node.tagName.toUpperCase() === uTagName) results.push(node);
+        return results;
+      } else return root.getElementsByTagName(tagName);
+    },
+
+    id: function(nodes, root, id, combinator) {
+      var targetNode = $(id), h = Selector.handlers;
+      if (!targetNode) return [];
+      if (!nodes && root == document) return [targetNode];
+      if (nodes) {
+        if (combinator) {
+          if (combinator == 'child') {
+            for (var i = 0, node; node = nodes[i]; i++)
+              if (targetNode.parentNode == node) return [targetNode];
+          } else if (combinator == 'descendant') {
+            for (var i = 0, node; node = nodes[i]; i++)
+              if (Element.descendantOf(targetNode, node)) return [targetNode];
+          } else if (combinator == 'adjacent') {
+            for (var i = 0, node; node = nodes[i]; i++)
+              if (Selector.handlers.previousElementSibling(targetNode) == node)
+                return [targetNode];
+          } else nodes = h[combinator](nodes);
+        }
+        for (var i = 0, node; node = nodes[i]; i++)
+          if (node == targetNode) return [targetNode];
+        return [];
+      }
+      return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
+    },
+
+    className: function(nodes, root, className, combinator) {
+      if (nodes && combinator) nodes = this[combinator](nodes);
+      return Selector.handlers.byClassName(nodes, root, className);
+    },
+
+    byClassName: function(nodes, root, className) {
+      if (!nodes) nodes = Selector.handlers.descendant([root]);
+      var needle = ' ' + className + ' ';
+      for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
+        nodeClassName = node.className;
+        if (nodeClassName.length == 0) continue;
+        if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
+          results.push(node);
+      }
+      return results;
+    },
+
+    attrPresence: function(nodes, root, attr, combinator) {
+      if (!nodes) nodes = root.getElementsByTagName("*");
+      if (nodes && combinator) nodes = this[combinator](nodes);
+      var results = [];
+      for (var i = 0, node; node = nodes[i]; i++)
+        if (Element.hasAttribute(node, attr)) results.push(node);
+      return results;
+    },
+
+    attr: function(nodes, root, attr, value, operator, combinator) {
+      if (!nodes) nodes = root.getElementsByTagName("*");
+      if (nodes && combinator) nodes = this[combinator](nodes);
+      var handler = Selector.operators[operator], results = [];
+      for (var i = 0, node; node = nodes[i]; i++) {
+        var nodeValue = Element.readAttribute(node, attr);
+        if (nodeValue === null) continue;
+        if (handler(nodeValue, value)) results.push(node);
+      }
+      return results;
+    },
+
+    pseudo: function(nodes, name, value, root, combinator) {
+      if (nodes && combinator) nodes = this[combinator](nodes);
+      if (!nodes) nodes = root.getElementsByTagName("*");
+      return Selector.pseudos[name](nodes, value, root);
+    }
+  },
+
+  pseudos: {
+    'first-child': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        if (Selector.handlers.previousElementSibling(node)) continue;
+          results.push(node);
+      }
+      return results;
+    },
+    'last-child': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        if (Selector.handlers.nextElementSibling(node)) continue;
+          results.push(node);
+      }
+      return results;
+    },
+    'only-child': function(nodes, value, root) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
+          results.push(node);
+      return results;
+    },
+    'nth-child':        function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root);
+    },
+    'nth-last-child':   function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root, true);
+    },
+    'nth-of-type':      function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root, false, true);
+    },
+    'nth-last-of-type': function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root, true, true);
+    },
+    'first-of-type':    function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, "1", root, false, true);
+    },
+    'last-of-type':     function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, "1", root, true, true);
+    },
+    'only-of-type':     function(nodes, formula, root) {
+      var p = Selector.pseudos;
+      return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
+    },
+
+    // handles the an+b logic
+    getIndices: function(a, b, total) {
+      if (a == 0) return b > 0 ? [b] : [];
+      return $R(1, total).inject([], function(memo, i) {
+        if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
+        return memo;
+      });
+    },
+
+    // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
+    nth: function(nodes, formula, root, reverse, ofType) {
+      if (nodes.length == 0) return [];
+      if (formula == 'even') formula = '2n+0';
+      if (formula == 'odd')  formula = '2n+1';
+      var h = Selector.handlers, results = [], indexed = [], m;
+      h.mark(nodes);
+      for (var i = 0, node; node = nodes[i]; i++) {
+        if (!node.parentNode._countedByPrototype) {
+          h.index(node.parentNode, reverse, ofType);
+          indexed.push(node.parentNode);
+        }
+      }
+      if (formula.match(/^\d+$/)) { // just a number
+        formula = Number(formula);
+        for (var i = 0, node; node = nodes[i]; i++)
+          if (node.nodeIndex == formula) results.push(node);
+      } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+        if (m[1] == "-") m[1] = -1;
+        var a = m[1] ? Number(m[1]) : 1;
+        var b = m[2] ? Number(m[2]) : 0;
+        var indices = Selector.pseudos.getIndices(a, b, nodes.length);
+        for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
+          for (var j = 0; j < l; j++)
+            if (node.nodeIndex == indices[j]) results.push(node);
+        }
+      }
+      h.unmark(nodes);
+      h.unmark(indexed);
+      return results;
+    },
+
+    'empty': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        // IE treats comments as element nodes
+        if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue;
+        results.push(node);
+      }
+      return results;
+    },
+
+    'not': function(nodes, selector, root) {
+      var h = Selector.handlers, selectorType, m;
+      var exclusions = new Selector(selector).findElements(root);
+      h.mark(exclusions);
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (!node._countedByPrototype) results.push(node);
+      h.unmark(exclusions);
+      return results;
+    },
+
+    'enabled': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (!node.disabled) results.push(node);
+      return results;
+    },
+
+    'disabled': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (node.disabled) results.push(node);
+      return results;
+    },
+
+    'checked': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (node.checked) results.push(node);
+      return results;
+    }
+  },
+
+  operators: {
+    '=':  function(nv, v) { return nv == v; },
+    '!=': function(nv, v) { return nv != v; },
+    '^=': function(nv, v) { return nv.startsWith(v); },
+    '$=': function(nv, v) { return nv.endsWith(v); },
+    '*=': function(nv, v) { return nv.include(v); },
+    '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
+    '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); }
+  },
+
+  split: function(expression) {
+    var expressions = [];
+    expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
+      expressions.push(m[1].strip());
+    });
+    return expressions;
+  },
+
+  matchElements: function(elements, expression) {
+    var matches = $$(expression), h = Selector.handlers;
+    h.mark(matches);
+    for (var i = 0, results = [], element; element = elements[i]; i++)
+      if (element._countedByPrototype) results.push(element);
+    h.unmark(matches);
+    return results;
+  },
+
+  findElement: function(elements, expression, index) {
+    if (Object.isNumber(expression)) {
+      index = expression; expression = false;
+    }
+    return Selector.matchElements(elements, expression || '*')[index || 0];
+  },
+
+  findChildElements: function(element, expressions) {
+    expressions = Selector.split(expressions.join(','));
+    var results = [], h = Selector.handlers;
+    for (var i = 0, l = expressions.length, selector; i < l; i++) {
+      selector = new Selector(expressions[i].strip());
+      h.concat(results, selector.findElements(element));
+    }
+    return (l > 1) ? h.unique(results) : results;
+  }
+});
+
+if (Prototype.Browser.IE) {
+  Object.extend(Selector.handlers, {
+    // IE returns comment nodes on getElementsByTagName("*").
+    // Filter them out.
+    concat: function(a, b) {
+      for (var i = 0, node; node = b[i]; i++)
+        if (node.tagName !== "!") a.push(node);
+      return a;
+    },
+
+    // IE improperly serializes _countedByPrototype in (inner|outer)HTML.
+    unmark: function(nodes) {
+      for (var i = 0, node; node = nodes[i]; i++)
+        node.removeAttribute('_countedByPrototype');
+      return nodes;
+    }
+  });
+}
+
+function $$() {
+  return Selector.findChildElements(document, $A(arguments));
+}
+var Form = {
+  reset: function(form) {
+    $(form).reset();
+    return form;
+  },
+
+  serializeElements: function(elements, options) {
+    if (typeof options != 'object') options = { hash: !!options };
+    else if (Object.isUndefined(options.hash)) options.hash = true;
+    var key, value, submitted = false, submit = options.submit;
+
+    var data = elements.inject({ }, function(result, element) {
+      if (!element.disabled && element.name) {
+        key = element.name; value = $(element).getValue();
+        if (value != null && (element.type != 'submit' || (!submitted &&
+            submit !== false && (!submit || key == submit) && (submitted = true)))) {
+          if (key in result) {
+            // a key is already present; construct an array of values
+            if (!Object.isArray(result[key])) result[key] = [result[key]];
+            result[key].push(value);
+          }
+          else result[key] = value;
+        }
+      }
+      return result;
+    });
+
+    return options.hash ? data : Object.toQueryString(data);
+  }
+};
+
+Form.Methods = {
+  serialize: function(form, options) {
+    return Form.serializeElements(Form.getElements(form), options);
+  },
+
+  getElements: function(form) {
+    return $A($(form).getElementsByTagName('*')).inject([],
+      function(elements, child) {
+        if (Form.Element.Serializers[child.tagName.toLowerCase()])
+          elements.push(Element.extend(child));
+        return elements;
+      }
+    );
+  },
+
+  getInputs: function(form, typeName, name) {
+    form = $(form);
+    var inputs = form.getElementsByTagName('input');
+
+    if (!typeName && !name) return $A(inputs).map(Element.extend);
+
+    for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
+      var input = inputs[i];
+      if ((typeName && input.type != typeName) || (name && input.name != name))
+        continue;
+      matchingInputs.push(Element.extend(input));
+    }
+
+    return matchingInputs;
+  },
+
+  disable: function(form) {
+    form = $(form);
+    Form.getElements(form).invoke('disable');
+    return form;
+  },
+
+  enable: function(form) {
+    form = $(form);
+    Form.getElements(form).invoke('enable');
+    return form;
+  },
+
+  findFirstElement: function(form) {
+    var elements = $(form).getElements().findAll(function(element) {
+      return 'hidden' != element.type && !element.disabled;
+    });
+    var firstByIndex = elements.findAll(function(element) {
+      return element.hasAttribute('tabIndex') && element.tabIndex >= 0;
+    }).sortBy(function(element) { return element.tabIndex }).first();
+
+    return firstByIndex ? firstByIndex : elements.find(function(element) {
+      return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+    });
+  },
+
+  focusFirstElement: function(form) {
+    form = $(form);
+    form.findFirstElement().activate();
+    return form;
+  },
+
+  request: function(form, options) {
+    form = $(form), options = Object.clone(options || { });
+
+    var params = options.parameters, action = form.readAttribute('action') || '';
+    if (action.blank()) action = window.location.href;
+    options.parameters = form.serialize(true);
+
+    if (params) {
+      if (Object.isString(params)) params = params.toQueryParams();
+      Object.extend(options.parameters, params);
+    }
+
+    if (form.hasAttribute('method') && !options.method)
+      options.method = form.method;
+
+    return new Ajax.Request(action, options);
+  }
+};
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element = {
+  focus: function(element) {
+    $(element).focus();
+    return element;
+  },
+
+  select: function(element) {
+    $(element).select();
+    return element;
+  }
+};
+
+Form.Element.Methods = {
+  serialize: function(element) {
+    element = $(element);
+    if (!element.disabled && element.name) {
+      var value = element.getValue();
+      if (value != undefined) {
+        var pair = { };
+        pair[element.name] = value;
+        return Object.toQueryString(pair);
+      }
+    }
+    return '';
+  },
+
+  getValue: function(element) {
+    element = $(element);
+    var method = element.tagName.toLowerCase();
+    return Form.Element.Serializers[method](element);
+  },
+
+  setValue: function(element, value) {
+    element = $(element);
+    var method = element.tagName.toLowerCase();
+    Form.Element.Serializers[method](element, value);
+    return element;
+  },
+
+  clear: function(element) {
+    $(element).value = '';
+    return element;
+  },
+
+  present: function(element) {
+    return $(element).value != '';
+  },
+
+  activate: function(element) {
+    element = $(element);
+    try {
+      element.focus();
+      if (element.select && (element.tagName.toLowerCase() != 'input' ||
+          !['button', 'reset', 'submit'].include(element.type)))
+        element.select();
+    } catch (e) { }
+    return element;
+  },
+
+  disable: function(element) {
+    element = $(element);
+    element.blur();
+    element.disabled = true;
+    return element;
+  },
+
+  enable: function(element) {
+    element = $(element);
+    element.disabled = false;
+    return element;
+  }
+};
+
+/*--------------------------------------------------------------------------*/
+
+var Field = Form.Element;
+var $F = Form.Element.Methods.getValue;
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element.Serializers = {
+  input: function(element, value) {
+    switch (element.type.toLowerCase()) {
+      case 'checkbox':
+      case 'radio':
+        return Form.Element.Serializers.inputSelector(element, value);
+      default:
+        return Form.Element.Serializers.textarea(element, value);
+    }
+  },
+
+  inputSelector: function(element, value) {
+    if (Object.isUndefined(value)) return element.checked ? element.value : null;
+    else element.checked = !!value;
+  },
+
+  textarea: function(element, value) {
+    if (Object.isUndefined(value)) return element.value;
+    else element.value = value;
+  },
+
+  select: function(element, index) {
+    if (Object.isUndefined(index))
+      return this[element.type == 'select-one' ?
+        'selectOne' : 'selectMany'](element);
+    else {
+      var opt, value, single = !Object.isArray(index);
+      for (var i = 0, length = element.length; i < length; i++) {
+        opt = element.options[i];
+        value = this.optionValue(opt);
+        if (single) {
+          if (value == index) {
+            opt.selected = true;
+            return;
+          }
+        }
+        else opt.selected = index.include(value);
+      }
+    }
+  },
+
+  selectOne: function(element) {
+    var index = element.selectedIndex;
+    return index >= 0 ? this.optionValue(element.options[index]) : null;
+  },
+
+  selectMany: function(element) {
+    var values, length = element.length;
+    if (!length) return null;
+
+    for (var i = 0, values = []; i < length; i++) {
+      var opt = element.options[i];
+      if (opt.selected) values.push(this.optionValue(opt));
+    }
+    return values;
+  },
+
+  optionValue: function(opt) {
+    // extend element because hasAttribute may not be native
+    return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
+  }
+};
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.TimedObserver = Class.create(PeriodicalExecuter, {
+  initialize: function($super, element, frequency, callback) {
+    $super(callback, frequency);
+    this.element   = $(element);
+    this.lastValue = this.getValue();
+  },
+
+  execute: function() {
+    var value = this.getValue();
+    if (Object.isString(this.lastValue) && Object.isString(value) ?
+        this.lastValue != value : String(this.lastValue) != String(value)) {
+      this.callback(this.element, value);
+      this.lastValue = value;
+    }
+  }
+});
+
+Form.Element.Observer = Class.create(Abstract.TimedObserver, {
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.Observer = Class.create(Abstract.TimedObserver, {
+  getValue: function() {
+    return Form.serialize(this.element);
+  }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.EventObserver = Class.create({
+  initialize: function(element, callback) {
+    this.element  = $(element);
+    this.callback = callback;
+
+    this.lastValue = this.getValue();
+    if (this.element.tagName.toLowerCase() == 'form')
+      this.registerFormCallbacks();
+    else
+      this.registerCallback(this.element);
+  },
+
+  onElementEvent: function() {
+    var value = this.getValue();
+    if (this.lastValue != value) {
+      this.callback(this.element, value);
+      this.lastValue = value;
+    }
+  },
+
+  registerFormCallbacks: function() {
+    Form.getElements(this.element).each(this.registerCallback, this);
+  },
+
+  registerCallback: function(element) {
+    if (element.type) {
+      switch (element.type.toLowerCase()) {
+        case 'checkbox':
+        case 'radio':
+          Event.observe(element, 'click', this.onElementEvent.bind(this));
+          break;
+        default:
+          Event.observe(element, 'change', this.onElementEvent.bind(this));
+          break;
+      }
+    }
+  }
+});
+
+Form.Element.EventObserver = Class.create(Abstract.EventObserver, {
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.EventObserver = Class.create(Abstract.EventObserver, {
+  getValue: function() {
+    return Form.serialize(this.element);
+  }
+});
+if (!window.Event) var Event = { };
+
+Object.extend(Event, {
+  KEY_BACKSPACE: 8,
+  KEY_TAB:       9,
+  KEY_RETURN:   13,
+  KEY_ESC:      27,
+  KEY_LEFT:     37,
+  KEY_UP:       38,
+  KEY_RIGHT:    39,
+  KEY_DOWN:     40,
+  KEY_DELETE:   46,
+  KEY_HOME:     36,
+  KEY_END:      35,
+  KEY_PAGEUP:   33,
+  KEY_PAGEDOWN: 34,
+  KEY_INSERT:   45,
+
+  cache: { },
+
+  relatedTarget: function(event) {
+    var element;
+    switch(event.type) {
+      case 'mouseover': element = event.fromElement; break;
+      case 'mouseout':  element = event.toElement;   break;
+      default: return null;
+    }
+    return Element.extend(element);
+  }
+});
+
+Event.Methods = (function() {
+  var isButton;
+
+  if (Prototype.Browser.IE) {
+    var buttonMap = { 0: 1, 1: 4, 2: 2 };
+    isButton = function(event, code) {
+      return event.button == buttonMap[code];
+    };
+
+  } else if (Prototype.Browser.WebKit) {
+    isButton = function(event, code) {
+      switch (code) {
+        case 0: return event.which == 1 && !event.metaKey;
+        case 1: return event.which == 1 && event.metaKey;
+        default: return false;
+      }
+    };
+
+  } else {
+    isButton = function(event, code) {
+      return event.which ? (event.which === code + 1) : (event.button === code);
+    };
+  }
+
+  return {
+    isLeftClick:   function(event) { return isButton(event, 0) },
+    isMiddleClick: function(event) { return isButton(event, 1) },
+    isRightClick:  function(event) { return isButton(event, 2) },
+
+    element: function(event) {
+      var node = Event.extend(event).target;
+      return Element.extend(node.nodeType == Node.TEXT_NODE ? node.parentNode : node);
+    },
+
+    findElement: function(event, expression) {
+      var element = Event.element(event);
+      if (!expression) return element;
+      var elements = [element].concat(element.ancestors());
+      return Selector.findElement(elements, expression, 0);
+    },
+
+    pointer: function(event) {
+      return {
+        x: event.pageX || (event.clientX +
+          (document.documentElement.scrollLeft || document.body.scrollLeft)),
+        y: event.pageY || (event.clientY +
+          (document.documentElement.scrollTop || document.body.scrollTop))
+      };
+    },
+
+    pointerX: function(event) { return Event.pointer(event).x },
+    pointerY: function(event) { return Event.pointer(event).y },
+
+    stop: function(event) {
+      Event.extend(event);
+      event.preventDefault();
+      event.stopPropagation();
+      event.stopped = true;
+    }
+  };
+})();
+
+Event.extend = (function() {
+  var methods = Object.keys(Event.Methods).inject({ }, function(m, name) {
+    m[name] = Event.Methods[name].methodize();
+    return m;
+  });
+
+  if (Prototype.Browser.IE) {
+    Object.extend(methods, {
+      stopPropagation: function() { this.cancelBubble = true },
+      preventDefault:  function() { this.returnValue = false },
+      inspect: function() { return "[object Event]" }
+    });
+
+    return function(event) {
+      if (!event) return false;
+      if (event._extendedByPrototype) return event;
+
+      event._extendedByPrototype = Prototype.emptyFunction;
+      var pointer = Event.pointer(event);
+      Object.extend(event, {
+        target: event.srcElement,
+        relatedTarget: Event.relatedTarget(event),
+        pageX:  pointer.x,
+        pageY:  pointer.y
+      });
+      return Object.extend(event, methods);
+    };
+
+  } else {
+    Event.prototype = Event.prototype || document.createEvent("HTMLEvents").__proto__;
+    Object.extend(Event.prototype, methods);
+    return Prototype.K;
+  }
+})();
+
+Object.extend(Event, (function() {
+  var cache = Event.cache;
+
+  function getEventID(element) {
+    if (element._prototypeEventID) return element._prototypeEventID[0];
+    arguments.callee.id = arguments.callee.id || 1;
+    return element._prototypeEventID = [++arguments.callee.id];
+  }
+
+  function getDOMEventName(eventName) {
+    if (eventName && eventName.include(':')) return "dataavailable";
+    return eventName;
+  }
+
+  function getCacheForID(id) {
+    return cache[id] = cache[id] || { };
+  }
+
+  function getWrappersForEventName(id, eventName) {
+    var c = getCacheForID(id);
+    return c[eventName] = c[eventName] || [];
+  }
+
+  function createWrapper(element, eventName, handler) {
+    var id = getEventID(element);
+    var c = getWrappersForEventName(id, eventName);
+    if (c.pluck("handler").include(handler)) return false;
+
+    var wrapper = function(event) {
+      if (!Event || !Event.extend ||
+        (event.eventName && event.eventName != eventName))
+          return false;
+
+      Event.extend(event);
+      handler.call(element, event);
+    };
+
+    wrapper.handler = handler;
+    c.push(wrapper);
+    return wrapper;
+  }
+
+  function findWrapper(id, eventName, handler) {
+    var c = getWrappersForEventName(id, eventName);
+    return c.find(function(wrapper) { return wrapper.handler == handler });
+  }
+
+  function destroyWrapper(id, eventName, handler) {
+    var c = getCacheForID(id);
+    if (!c[eventName]) return false;
+    c[eventName] = c[eventName].without(findWrapper(id, eventName, handler));
+  }
+
+  function destroyCache() {
+    for (var id in cache)
+      for (var eventName in cache[id])
+        cache[id][eventName] = null;
+  }
+
+  if (window.attachEvent) {
+    window.attachEvent("onunload", destroyCache);
+  }
+
+  return {
+    observe: function(element, eventName, handler) {
+      element = $(element);
+      var name = getDOMEventName(eventName);
+
+      var wrapper = createWrapper(element, eventName, handler);
+      if (!wrapper) return element;
+
+      if (element.addEventListener) {
+        element.addEventListener(name, wrapper, false);
+      } else {
+        element.attachEvent("on" + name, wrapper);
+      }
+
+      return element;
+    },
+
+    stopObserving: function(element, eventName, handler) {
+      element = $(element);
+      var id = getEventID(element), name = getDOMEventName(eventName);
+
+      if (!handler && eventName) {
+        getWrappersForEventName(id, eventName).each(function(wrapper) {
+          element.stopObserving(eventName, wrapper.handler);
+        });
+        return element;
+
+      } else if (!eventName) {
+        Object.keys(getCacheForID(id)).each(function(eventName) {
+          element.stopObserving(eventName);
+        });
+        return element;
+      }
+
+      var wrapper = findWrapper(id, eventName, handler);
+      if (!wrapper) return element;
+
+      if (element.removeEventListener) {
+        element.removeEventListener(name, wrapper, false);
+      } else {
+        element.detachEvent("on" + name, wrapper);
+      }
+
+      destroyWrapper(id, eventName, handler);
+
+      return element;
+    },
+
+    fire: function(element, eventName, memo) {
+      element = $(element);
+      if (element == document && document.createEvent && !element.dispatchEvent)
+        element = document.documentElement;
+
+      var event;
+      if (document.createEvent) {
+        event = document.createEvent("HTMLEvents");
+        event.initEvent("dataavailable", true, true);
+      } else {
+        event = document.createEventObject();
+        event.eventType = "ondataavailable";
+      }
+
+      event.eventName = eventName;
+      event.memo = memo || { };
+
+      if (document.createEvent) {
+        element.dispatchEvent(event);
+      } else {
+        element.fireEvent(event.eventType, event);
+      }
+
+      return Event.extend(event);
+    }
+  };
+})());
+
+Object.extend(Event, Event.Methods);
+
+Element.addMethods({
+  fire:          Event.fire,
+  observe:       Event.observe,
+  stopObserving: Event.stopObserving
+});
+
+Object.extend(document, {
+  fire:          Element.Methods.fire.methodize(),
+  observe:       Element.Methods.observe.methodize(),
+  stopObserving: Element.Methods.stopObserving.methodize(),
+  loaded:        false
+});
+
+(function() {
+  /* Support for the DOMContentLoaded event is based on work by Dan Webb,
+     Matthias Miller, Dean Edwards and John Resig. */
+
+  var timer;
+
+  function fireContentLoadedEvent() {
+    if (document.loaded) return;
+    if (timer) window.clearInterval(timer);
+    document.fire("dom:loaded");
+    document.loaded = true;
+  }
+
+  if (document.addEventListener) {
+    if (Prototype.Browser.WebKit) {
+      timer = window.setInterval(function() {
+        if (/loaded|complete/.test(document.readyState))
+          fireContentLoadedEvent();
+      }, 0);
+
+      Event.observe(window, "load", fireContentLoadedEvent);
+
+    } else {
+      document.addEventListener("DOMContentLoaded",
+        fireContentLoadedEvent, false);
+    }
+
+  } else {
+    document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>");
+    $("__onDOMContentLoaded").onreadystatechange = function() {
+      if (this.readyState == "complete") {
+        this.onreadystatechange = null;
+        fireContentLoadedEvent();
+      }
+    };
+  }
+})();
+/*------------------------------- DEPRECATED -------------------------------*/
+
+Hash.toQueryString = Object.toQueryString;
+
+var Toggle = { display: Element.toggle };
+
+Element.Methods.childOf = Element.Methods.descendantOf;
+
+var Insertion = {
+  Before: function(element, content) {
+    return Element.insert(element, {before:content});
+  },
+
+  Top: function(element, content) {
+    return Element.insert(element, {top:content});
+  },
+
+  Bottom: function(element, content) {
+    return Element.insert(element, {bottom:content});
+  },
+
+  After: function(element, content) {
+    return Element.insert(element, {after:content});
+  }
+};
+
+var $continue = new Error('"throw $continue" is deprecated, use "return" instead');
+
+// This should be moved to script.aculo.us; notice the deprecated methods
+// further below, that map to the newer Element methods.
+var Position = {
+  // set to true if needed, warning: firefox performance problems
+  // NOT neeeded for page scrolling, only if draggable contained in
+  // scrollable elements
+  includeScrollOffsets: false,
+
+  // must be called before calling withinIncludingScrolloffset, every time the
+  // page is scrolled
+  prepare: function() {
+    this.deltaX =  window.pageXOffset
+                || document.documentElement.scrollLeft
+                || document.body.scrollLeft
+                || 0;
+    this.deltaY =  window.pageYOffset
+                || document.documentElement.scrollTop
+                || document.body.scrollTop
+                || 0;
+  },
+
+  // caches x/y coordinate pair to use with overlap
+  within: function(element, x, y) {
+    if (this.includeScrollOffsets)
+      return this.withinIncludingScrolloffsets(element, x, y);
+    this.xcomp = x;
+    this.ycomp = y;
+    this.offset = Element.cumulativeOffset(element);
+
+    return (y >= this.offset[1] &&
+            y <  this.offset[1] + element.offsetHeight &&
+            x >= this.offset[0] &&
+            x <  this.offset[0] + element.offsetWidth);
+  },
+
+  withinIncludingScrolloffsets: function(element, x, y) {
+    var offsetcache = Element.cumulativeScrollOffset(element);
+
+    this.xcomp = x + offsetcache[0] - this.deltaX;
+    this.ycomp = y + offsetcache[1] - this.deltaY;
+    this.offset = Element.cumulativeOffset(element);
+
+    return (this.ycomp >= this.offset[1] &&
+            this.ycomp <  this.offset[1] + element.offsetHeight &&
+            this.xcomp >= this.offset[0] &&
+            this.xcomp <  this.offset[0] + element.offsetWidth);
+  },
+
+  // within must be called directly before
+  overlap: function(mode, element) {
+    if (!mode) return 0;
+    if (mode == 'vertical')
+      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
+        element.offsetHeight;
+    if (mode == 'horizontal')
+      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
+        element.offsetWidth;
+  },
+
+  // Deprecation layer -- use newer Element methods now (1.5.2).
+
+  cumulativeOffset: Element.Methods.cumulativeOffset,
+
+  positionedOffset: Element.Methods.positionedOffset,
+
+  absolutize: function(element) {
+    Position.prepare();
+    return Element.absolutize(element);
+  },
+
+  relativize: function(element) {
+    Position.prepare();
+    return Element.relativize(element);
+  },
+
+  realOffset: Element.Methods.cumulativeScrollOffset,
+
+  offsetParent: Element.Methods.getOffsetParent,
+
+  page: Element.Methods.viewportOffset,
+
+  clone: function(source, target, options) {
+    options = options || { };
+    return Element.clonePosition(target, source, options);
+  }
+};
+
+/*--------------------------------------------------------------------------*/
+
+if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){
+  function iter(name) {
+    return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]";
+  }
+
+  instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ?
+  function(element, className) {
+    className = className.toString().strip();
+    var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className);
+    return cond ? document._getElementsByXPath('.//*' + cond, element) : [];
+  } : function(element, className) {
+    className = className.toString().strip();
+    var elements = [], classNames = (/\s/.test(className) ? $w(className) : null);
+    if (!classNames && !className) return elements;
+
+    var nodes = $(element).getElementsByTagName('*');
+    className = ' ' + className + ' ';
+
+    for (var i = 0, child, cn; child = nodes[i]; i++) {
+      if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) ||
+          (classNames && classNames.all(function(name) {
+            return !name.toString().blank() && cn.include(' ' + name + ' ');
+          }))))
+        elements.push(Element.extend(child));
+    }
+    return elements;
+  };
+
+  return function(className, parentElement) {
+    return $(parentElement || document.body).getElementsByClassName(className);
+  };
+}(Element.Methods);
+
+/*--------------------------------------------------------------------------*/
+
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+  initialize: function(element) {
+    this.element = $(element);
+  },
+
+  _each: function(iterator) {
+    this.element.className.split(/\s+/).select(function(name) {
+      return name.length > 0;
+    })._each(iterator);
+  },
+
+  set: function(className) {
+    this.element.className = className;
+  },
+
+  add: function(classNameToAdd) {
+    if (this.include(classNameToAdd)) return;
+    this.set($A(this).concat(classNameToAdd).join(' '));
+  },
+
+  remove: function(classNameToRemove) {
+    if (!this.include(classNameToRemove)) return;
+    this.set($A(this).without(classNameToRemove).join(' '));
+  },
+
+  toString: function() {
+    return $A(this).join(' ');
+  }
+};
+
+Object.extend(Element.ClassNames.prototype, Enumerable);
+
+/*--------------------------------------------------------------------------*/
+
+Element.addMethods();
\ No newline at end of file
diff --git a/babel/js/redbox.js b/babel/js/redbox.js
new file mode 100644 (file)
index 0000000..b3a1439
--- /dev/null
@@ -0,0 +1,130 @@
+/**
+ * Redbox.js
+ *
+ * $Horde: horde/js/src/redbox.js,v 1.10 2008/02/06 09:02:37 slusarz Exp $
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ */
+
+var RedBox = {
+
+    overlay: true,
+    onDisplay: null,
+
+    showInline: function(id)
+    {
+        this.appearWindow();
+        this.cloneWindowContents(id);
+    },
+
+    showHtml: function(html)
+    {
+        this.appearWindow();
+        this.htmlWindowContents(html);
+    },
+
+    appearWindow: function()
+    {
+        var loading = $('RB_loading');
+        if (loading && loading.visible()) {
+            loading.hide();
+        } else {
+            this.showOverlay();
+        }
+        var opts = { duration: 0.4, queue: 'end' },
+            w = $('RB_window');
+        if (this.onDisplay) {
+            opts.afterFinish = this.onDisplay;
+        }
+        new Effect.Appear(w, opts);
+        w.scrollTo();
+    },
+
+    loading: function()
+    {
+        this.showOverlay();
+        var rl = $('RB_loading');
+        if (rl) {
+            rl.show();
+        }
+        this.setWindowPosition();
+    },
+
+    close: function()
+    {
+        new Effect.Fade('RB_window', { duration: 0.4 });
+        if (this.overlay) {
+            new Effect.Fade('RB_overlay', { duration: 0.4 });
+        }
+    },
+
+    showOverlay: function()
+    {
+        var rb = $('RB_redbox');
+        if (!rb) {
+            rb = new Element('DIV', { id: 'RB_redbox', align: 'center' });
+            $(document.body).insert(rb);
+
+            var ov = new Element('DIV', { id: 'RB_overlay' }).hide();
+            rb.insert({ top: new Element('DIV', { id: 'RB_window' }).hide() }).insert({ top: ov });
+
+            if (this.overlay) {
+                ov.insert({ top: new Element('DIV', { id: 'RB_loading' }).hide() });
+            }
+        }
+
+        if (this.overlay) {
+            this.setOverlaySize();
+            new Effect.Appear('RB_overlay', { duration: 0.4, to: 0.6, queue: 'end' });
+        }
+    },
+
+    setOverlaySize: function()
+    {
+        if (window.innerHeight && window.scrollMaxY) {
+            yScroll = window.innerHeight + window.scrollMaxY;
+        } else if (document.body.scrollHeight > document.body.offsetHeight) {
+            // all but Explorer Mac
+            yScroll = document.body.scrollHeight;
+        } else {
+            // Explorer Mac...would also work in Explorer 6 Strict, Mozilla
+            // and Safari
+            yScroll = document.body.offsetHeight;
+        }
+        Element.setStyle('RB_overlay', { height: yScroll + 'px' });
+    },
+
+    setWindowPosition: function()
+    {
+        var win = $('RB_window');
+        var d = win.getDimensions(),
+            v = document.viewport.getDimensions();
+        win.setStyle({ width: 'auto', height: 'auto', left: ((v.width - d.width) / 2) + 'px', top: ((v.height - d.height) / 2) + 'px' });
+    },
+
+    cloneWindowContents: function(id)
+    {
+        $('RB_window').appendChild($($(id).cloneNode(true)).setStyle({ display: 'block' }));
+        this.setWindowPosition();
+    },
+
+    htmlWindowContents: function(html)
+    {
+        $('RB_window').update(html);
+        this.setWindowPosition();
+    },
+
+    getWindowContents: function()
+    {
+        var w = $('RB_window');
+        return w.visible() ? w.down() : null;
+    },
+
+    overlayVisible: function()
+    {
+        var ov = $('RB_overlay');
+        return ov && ov.visible();
+    }
+
+}
diff --git a/babel/lib/Display.php b/babel/lib/Display.php
new file mode 100644 (file)
index 0000000..877c9e1
--- /dev/null
@@ -0,0 +1,136 @@
+<?php
+/**
+ * Copyright 2000-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author  Joel Vandal <joel@scopserv.com>
+ * @package Babel
+ */
+
+class Translate_Display {
+    
+    function header($msg, $msg2 = '') {
+       global $cnt_i, $registry;
+       $select_img = Horde::img('alerts/message.png', '', '', $registry->getImageDir('horde'));
+       print sprintf('<table cellspacing=0 cellpadding=0 width=100%%><tr><td class="header">%s</td><td align="right" class="header">%s</td><td  class="header" width="20">%s</td></tr></table>', $msg, $msg2, $select_img);
+       flush();
+    }
+    
+    function warning($msg, $bold = true) {
+       global $cnt_i, $registry;
+       $item = ($cnt_i++ % 2);
+       $select_img = Horde::img('alerts/warning.png', '', '', $registry->getImageDir('horde'));
+       if ($bold) {
+           print sprintf('<table cellspacing=0 cellpadding=0 width=100%%><tr><td class="item%d" style="color: #ff0000">%s</td><td  class="item%1$d" width="20">%s</td></tr></table>', $item, $msg, $select_img);
+       } else {
+           print sprintf('<table cellspacing=0 cellpadding=0 width=100%%><tr><td class="item%d small" style="color: #ff0000">%s</td><td  class="item%1$d" width="20">%s</td></tr></table>', $item, $msg, $select_img);
+       }
+       flush();
+    }
+    
+    function error($msg) {
+       global $cnt_i, $registry;
+       $item = ($cnt_i++ % 2);
+       $select_img = Horde::img('alerts/error.png', '', '', $registry->getImageDir('horde'));
+       print sprintf('<table cellspacing=0 cellpadding=0 width=100%%><tr><td class="item%d" style="color: #ff0000"><b>%s</b></td><td  class="item%1$d" width="20">%s</td></tr></table>', $item, $msg, $select_img);
+       flush();
+    }
+    
+    function info($msg = "", $bold = true) {
+       
+       global $cnt_i, $registry;
+       
+       if (empty($msg)) {
+           echo "<br />";
+       } else {
+           
+           $item = ($cnt_i++ % 2);
+           
+           $select_img = Horde::img('select.png', '', '', $registry->getImageDir('horde'));
+           if ($bold) {
+               print sprintf('<table cellspacing=0 cellpadding=0 width=100%%><tr><td class="item%d"><b>%s</b></td><td  class="item%1$d" width="20">%s</td></tr></table>', $item, $msg, $select_img);
+           } else {
+               print sprintf('<table cellspacing=0 cellpadding=0 width=100%%><tr><td class="item%d small">%s</td><td  class="item%1$d" width="20">%s</td></tr></table>', $item, $msg, '');
+           }
+           flush();
+       }
+    }
+
+    function parseCharset($headers)
+    {
+        if (preg_match('/charset=(.*)/i', $headers, $m)) {
+            return $m[1];
+        }
+       return NLS::getCharset();
+    }
+    
+    function convert_string($msg) {
+       global $po;
+
+       $f = array('/&lt;/', '/&gt;/');
+       $t = array('<', '>');
+       $msg = preg_replace($f, $t, $msg);
+       return String::convertCharset(html_entity_decode($msg), NLS::getCharset(), Translate_Display::parseCharset($po->meta['Content-Type']));
+    }
+    
+    function display_string($msg) {
+       global $po;
+
+       $f = array('/</', '/>/');
+       $t = array('&lt;', '&gt;');
+       $msg = preg_replace($f, $t, $msg);
+       return String::convertCharset($msg, Translate_Display::parseCharset($po->meta['Content-Type']), NLS::getCharset());
+    }
+    
+    function get_percent($used, $total) {
+       if ($total > 0) {
+           $percent = sprintf("%2.2f", (($used * 100) / $total));
+       } else {
+           $percent = 0;
+       }
+       
+       return $percent;
+    }
+    
+    function create_bargraph ($used, $total, $text = true, $reverse = false, $small = false) {
+       if ($total > 0) {
+           $percent = round(($used * 100) / $total);
+       } else {
+           $percent = 0;
+       }
+       
+       $html = '<table border="0" cellpadding="0" cellspacing="0"><tr><td nowrap="nowrap">';
+       $html .= '<table border="0" width="100" cellpadding="0" cellspacing="0">';
+       $html .= '<tr height="10">';
+       
+       if ($percent > 0) {
+           $html .= '<td nowrap="nowrap" width="' . ($percent) . '" bgcolor="#00FF00"></td>';
+       }
+       
+       if ($percent != 100) {
+           $html .= '<td nowrap="nowrap" width="' . (100 - $percent) . '" ';
+           if ($reverse) {
+               $html .= ' bgcolor="#FFFFFF"></td>';
+           } else {
+               $html .= ' bgcolor="#006699"></td>';
+           }
+       }
+       
+       $html .= '</tr></table></td>';
+       
+       if ($text) {
+           if ($small) {
+               $html .= '<td class="small"> ' . $percent .'% </td>';
+           } else {
+               $html .= '<td> ' . $percent .'% </td>';
+           }
+       }
+       
+       $html .= '</tr></table>';
+       
+       return $html;    
+    }
+    
+}
diff --git a/babel/lib/Gettext.php b/babel/lib/Gettext.php
new file mode 100644 (file)
index 0000000..12b4019
--- /dev/null
@@ -0,0 +1,280 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * File::Gettext
+ * 
+ * PHP versions 4 and 5
+ *
+ * @category   FileFormats
+ * @package    File_Gettext
+ * @author     Michael Wallner <mike@php.net>
+ * @copyright  2004-2005 Michael Wallner
+ * @license    BSD, revised
+ * @version    CVS: $Id: Gettext.php,v 1.7 2005/11/08 18:57:03 mike Exp $
+ * @link       http://pear.php.net/package/File_Gettext
+ */
+
+/**
+ * Use PHPs builtin error messages
+ */
+ini_set('track_errors', true);
+
+/** 
+ * File_Gettext
+ * 
+ * GNU gettext file reader and writer.
+ * 
+ * #################################################################
+ * # All protected members of this class are public in its childs. #
+ * #################################################################
+ *
+ * @author      Michael Wallner <mike@php.net>
+ * @version     $Revision: 1.7 $
+ * @access      public
+ */
+class File_Gettext
+{
+    /**
+     * strings
+     * 
+     * associative array with all [msgid => msgstr] entries
+     * 
+     * @access  protected
+     * @var     array
+    */
+    var $strings = array();
+    var $encstr = array();
+    var $refs = array();
+    var $status = array();
+    var $comments = array();
+    
+    /**
+     * meta
+     * 
+     * associative array containing meta 
+     * information like project name or content type
+     * 
+     * @access  protected
+     * @var     array
+     */
+    var $meta = array();
+    
+    /**
+     * file path
+     * 
+     * @access  protected
+     * @var     string
+     */
+    var $file = '';
+    
+    /**
+     * Factory
+     *
+     * @static
+     * @access  public
+     * @return  object  Returns File_Gettext_PO or File_Gettext_MO on success 
+     *                  or PEAR_Error on failure.
+     * @param   string  $format MO or PO
+     * @param   string  $file   path to GNU gettext file
+     */
+    function &factory($format, $file = '')
+    {
+        $format = strToUpper($format);
+        if (!@include_once 'File/Gettext/' . $format . '.php') {
+            return File_Gettext::raiseError($php_errormsg);
+        }
+        $class = 'File_Gettext_' . $format;
+        $obref = &new $class($file);
+        return $obref;
+    }
+
+    /**
+     * poFile2moFile
+     *
+     * That's a simple fake of the 'msgfmt' console command.  It reads the
+     * contents of a GNU PO file and saves them to a GNU MO file.
+     * 
+     * @static
+     * @access  public
+     * @return  mixed   Returns true on success or PEAR_Error on failure.
+     * @param   string  $pofile path to GNU PO file
+     * @param   string  $mofile path to GNU MO file
+     */
+    function poFile2moFile($pofile, $mofile)
+    {
+        if (!is_file($pofile)) {
+            return File_Gettext::raiseError("File $pofile doesn't exist.");
+        }
+        
+        include_once 'File/Gettext/PO.php';
+        
+        $PO = &new File_Gettext_PO($pofile);
+        if (true !== ($e = $PO->load())) {
+            return $e;
+        }
+        
+        $MO = &$PO->toMO();
+        if (true !== ($e = $MO->save($mofile))) {
+            return $e;
+        }
+        unset($PO, $MO);
+        
+        return true;
+    }
+    
+    /**
+     * prepare
+     *
+     * @static
+     * @access  protected
+     * @return  string
+     * @param   string  $string
+     * @param   bool    $reverse
+     */
+    function prepare($string, $reverse = false)
+    {
+        if ($reverse) {
+            $smap = array('"', "\n", "\t", "\r");
+            $rmap = array('\\"', '\\n"' . "\n" . '"', '\\t', '\\r');
+            return (string) str_replace($smap, $rmap, $string);
+        } else {
+            $smap = array('/"\s+"/', '/\\\\n/', '/\\\\r/', '/\\\\t/', '/\\\\"/');
+            $rmap = array('', "\n", "\r", "\t", '"');
+            return (string) preg_replace($smap, $rmap, $string);
+        }
+    }
+    
+    /**
+     * meta2array
+     *
+     * @static
+     * @access  public
+     * @return  array
+     * @param   string  $meta
+     */
+    function meta2array($meta)
+    {
+        $array = array();
+        foreach (explode("\n", $meta) as $info) {
+            if ($info = trim($info)) {
+                list($key, $value) = explode(':', $info, 2);
+                $array[trim($key)] = trim($value);
+            }
+        }
+        return $array;
+    }
+
+    /**
+     * toArray
+     * 
+     * Returns meta info and strings as an array of a structure like that:
+     * <code>
+     *   array(
+     *       'meta' => array(
+     *           'Content-Type'      => 'text/plain; charset=iso-8859-1',
+     *           'Last-Translator'   => 'Michael Wallner <mike@iworks.at>',
+     *           'PO-Revision-Date'  => '2004-07-21 17:03+0200',
+     *           'Language-Team'     => 'German <mail@example.com>',
+     *       ),
+     *       'strings' => array(
+     *           'All rights reserved'   => 'Alle Rechte vorbehalten',
+     *           'Welcome'               => 'Willkommen',
+     *           // ...
+     *       )
+     *   )
+     * </code>
+     * 
+     * @see     fromArray()
+     * @access  protected
+     * @return  array
+     */
+    function toArray()
+    {
+       return array('meta' => $this->meta, 'strings' => $this->strings);
+    }
+    
+    /**
+     * fromArray
+     * 
+     * Assigns meta info and strings from an array of a structure like that:
+     * <code>
+     *   array(
+     *       'meta' => array(
+     *           'Content-Type'      => 'text/plain; charset=iso-8859-1',
+     *           'Last-Translator'   => 'Michael Wallner <mike@iworks.at>',
+     *           'PO-Revision-Date'  => date('Y-m-d H:iO'),
+     *           'Language-Team'     => 'German <mail@example.com>',
+     *       ),
+     *       'strings' => array(
+     *           'All rights reserved'   => 'Alle Rechte vorbehalten',
+     *           'Welcome'               => 'Willkommen',
+     *           // ...
+     *       )
+     *   )
+     * </code>
+     * 
+     * @see     toArray()
+     * @access  protected
+     * @return  bool
+     * @param   array       $array
+     */
+    function fromArray($array)
+    {
+       if (!array_key_exists('strings', $array)) {
+           if (count($array) != 2) {
+                return false;
+           } else {
+               list($this->meta, $this->strings) = $array;
+            }
+       } else {
+            $this->meta = @$array['meta'];
+            $this->strings = @$array['strings'];
+        }
+        return true;
+    }
+    
+    /**
+     * toMO
+     *
+     * @access  protected
+     * @return  object  File_Gettext_MO
+     */
+    function &toMO()
+    {
+        include_once 'File/Gettext/MO.php';
+        $MO = &new File_Gettext_MO;
+        $MO->fromArray($this->toArray());
+        return $MO;
+    }
+    
+    /**
+     * toPO
+     *
+     * @access  protected
+     * @return  object      File_Gettext_PO
+     */
+    function &toPO()
+    {
+        include_once 'File/Gettext/PO.php';
+        $PO = &new File_Gettext_PO;
+        $PO->fromArray($this->toArray());
+        return $PO;
+    }
+    
+    /**
+     * Raise PEAR error
+     *
+     * @static
+     * @access  protected
+     * @return  object
+     * @param   string  $error
+     * @param   int     $code
+     */
+    function raiseError($error = null, $code = null)
+    {
+        include_once 'PEAR.php';
+        return PEAR::raiseError($error, $code);
+    }
+}
+?>
diff --git a/babel/lib/Gettext/MO.php b/babel/lib/Gettext/MO.php
new file mode 100644 (file)
index 0000000..521b489
--- /dev/null
@@ -0,0 +1,331 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * File::Gettext
+ * 
+ * PHP versions 4 and 5
+ *
+ * @category   FileFormats
+ * @package    File_Gettext
+ * @author     Michael Wallner <mike@php.net>
+ * @copyright  2004-2005 Michael Wallner
+ * @license    BSD, revised
+ * @version    CVS: $Id: MO.php,v 1.8 2006/01/07 09:45:25 mike Exp $
+ * @link       http://pear.php.net/package/File_Gettext
+ */
+
+/**
+ * Requires File_Gettext
+ */
+require_once 'File/Gettext.php';
+
+/** 
+ * File_Gettext_MO
+ * 
+ * GNU MO file reader and writer.
+ *
+ * @author      Michael Wallner <mike@php.net>
+ * @version     $Revision: 1.8 $
+ * @access      public
+ */
+class File_Gettext_MO extends File_Gettext
+{
+    /**
+     * file handle
+     * 
+     * @access  private
+     * @var     resource
+     */
+    var $_handle = null;
+    
+    /**
+     * big endianess
+     * 
+     * Whether to write with big endian byte order.
+     * 
+     * @access  public
+     * @var     bool
+     */
+    var $writeBigEndian = false;
+    
+    /**
+     * Constructor
+     *
+     * @access  public
+     * @return  object      File_Gettext_MO
+     * @param   string      $file   path to GNU MO file
+     */
+    function File_Gettext_MO($file = '')
+    {
+        $this->file = $file;
+    }
+
+    /**
+     * _read
+     *
+     * @access  private
+     * @return  mixed
+     * @param   int     $bytes
+     */
+    function _read($bytes = 1)
+    {
+        if (0 < $bytes = abs($bytes)) {
+            return fread($this->_handle, $bytes);
+        }
+        return null;
+    }
+    
+    /**
+     * _readInt
+     *
+     * @access  private
+     * @return  int
+     * @param   bool    $bigendian
+     */
+    function _readInt($bigendian = false)
+    {
+        return current($array = unpack($bigendian ? 'N' : 'V', $this->_read(4)));
+    }
+    
+    /**
+     * _writeInt
+     *
+     * @access  private
+     * @return  int
+     * @param   int     $int
+     */
+    function _writeInt($int)
+    {
+        return $this->_write(pack($this->writeBigEndian ? 'N' : 'V', (int) $int));
+    }
+    
+    /**
+     * _write
+     *
+     * @access  private
+     * @return  int
+     * @param   string  $data
+     */
+    function _write($data)
+    {
+        return fwrite($this->_handle, $data);
+    }
+    
+    /**
+     * _writeStr
+     *
+     * @access  private
+     * @return  int
+     * @param   string  $string
+     */
+    function _writeStr($string)
+    {
+        return $this->_write($string . "\0");
+    }
+    
+    /**
+     * _readStr
+     *
+     * @access  private
+     * @return  string
+     * @param   array   $params     associative array with offset and length 
+     *                              of the string
+     */
+    function _readStr($params)
+    {
+        fseek($this->_handle, $params['offset']);
+        return $this->_read($params['length']);
+    }
+    
+    /**
+     * Load MO file
+     *
+     * @access   public
+     * @return   mixed   Returns true on success or PEAR_Error on failure.
+     * @param    string  $file
+     */
+    function load($file = null)
+    {
+        $this->strings = array();
+        
+        if (!isset($file)) {
+            $file = $this->file;
+        }
+        
+        // open MO file
+        if (!is_resource($this->_handle = @fopen($file, 'rb'))) {
+            return parent::raiseError($php_errormsg . ' ' . $file);
+        }
+        // lock MO file shared
+        if (!@flock($this->_handle, LOCK_SH)) {
+            @fclose($this->_handle);
+            return parent::raiseError($php_errormsg . ' ' . $file);
+        }
+        
+        // read (part of) magic number from MO file header and define endianess
+        switch ($magic = current($array = unpack('c', $this->_read(4))))
+        {
+            case -34:
+                $be = false;
+            break;
+            
+            case -107:
+                $be = true;
+            break;
+            
+            default:
+                return parent::raiseError("No GNU mo file: $file (magic: $magic)");
+        }
+
+        // check file format revision - we currently only support 0
+        if (0 !== ($_rev = $this->_readInt($be))) {
+            return parent::raiseError('Invalid file format revision: ' . $_rev);
+        }
+       
+        // count of strings in this file
+        $count = $this->_readInt($be);
+        
+        // offset of hashing table of the msgids
+        $offset_original = $this->_readInt($be);
+        // offset of hashing table of the msgstrs
+        $offset_translat = $this->_readInt($be);
+        
+        // move to msgid hash table
+        fseek($this->_handle, $offset_original);
+        // read lengths and offsets of msgids
+        $original = array();
+        for ($i = 0; $i < $count; $i++) {
+            $original[$i] = array(
+                'length' => $this->_readInt($be),
+                'offset' => $this->_readInt($be)
+            );
+        }
+        
+        // move to msgstr hash table
+        fseek($this->_handle, $offset_translat);
+        // read lengths and offsets of msgstrs
+        $translat = array();
+        for ($i = 0; $i < $count; $i++) {
+            $translat[$i] = array(
+                'length' => $this->_readInt($be),
+                'offset' => $this->_readInt($be)
+            );
+        }
+        
+        // read all
+        for ($i = 0; $i < $count; $i++) {
+            $this->strings[$this->_readStr($original[$i])] = 
+                $this->_readStr($translat[$i]);
+        }
+        
+        // done
+        @flock($this->_handle, LOCK_UN);
+        @fclose($this->_handle);
+        $this->_handle = null;
+        
+        // check for meta info
+        if (isset($this->strings[''])) {
+            $this->meta = parent::meta2array($this->strings['']);
+            unset($this->strings['']);
+        }
+        
+        return true;
+    }
+    
+    /**
+     * Save MO file
+     *
+     * @access  public
+     * @return  mixed   Returns true on success or PEAR_Error on failure.
+     * @param   string  $file
+     */
+    function save($file = null)
+    {
+        if (!isset($file)) {
+            $file = $this->file;
+        }
+        
+        // open MO file
+        if (!is_resource($this->_handle = @fopen($file, 'wb'))) {
+            return parent::raiseError($php_errormsg . ' ' . $file);
+        }
+        // lock MO file exclusively
+        if (!@flock($this->_handle, LOCK_EX)) {
+            @fclose($this->_handle);
+            return parent::raiseError($php_errormsg . ' ' . $file);
+        }
+        
+        // write magic number
+        if ($this->writeBigEndian) {
+            $this->_write(pack('c*', 0x95, 0x04, 0x12, 0xde));
+        } else {
+            $this->_write(pack('c*', 0xde, 0x12, 0x04, 0x95));
+        }
+        
+        // write file format revision
+        $this->_writeInt(0);
+        
+        $count = count($this->strings) + ($meta = (count($this->meta) ? 1 : 0));
+        // write count of strings
+        $this->_writeInt($count);
+        
+        $offset = 28;
+        // write offset of orig. strings hash table
+        $this->_writeInt($offset);
+        
+        $offset += ($count * 8);
+        // write offset transl. strings hash table
+        $this->_writeInt($offset);
+        
+        // write size of hash table (we currently ommit the hash table)
+        $this->_writeInt(0);
+        
+        $offset += ($count * 8);
+        // write offset of hash table
+        $this->_writeInt($offset);
+        
+        // unshift meta info
+        if ($meta) {
+            $meta = '';
+            foreach ($this->meta as $key => $val) {
+                $meta .= $key . ': ' . $val . "\n";
+            }
+            $strings = array('' => $meta) + $this->strings;
+        } else {
+            $strings = $this->strings;
+        }
+        
+        // write offsets for original strings
+        foreach (array_keys($strings) as $o) {
+            $len = strlen($o);
+            $this->_writeInt($len);
+            $this->_writeInt($offset);
+            $offset += $len + 1;
+        }
+        
+        // write offsets for translated strings
+        foreach ($strings as $t) {
+            $len = strlen($t);
+            $this->_writeInt($len);
+            $this->_writeInt($offset);
+            $offset += $len + 1;
+        }
+
+        // write original strings
+        foreach (array_keys($strings) as $o) {
+            $this->_writeStr($o);
+        }
+
+        // write translated strings
+        foreach ($strings as $t) {
+            $this->_writeStr($t);
+        }
+        
+        // done
+        @flock($this->_handle, LOCK_UN);
+        @fclose($this->_handle);
+        return true;
+    }
+}
+?>
diff --git a/babel/lib/Gettext/PO.php b/babel/lib/Gettext/PO.php
new file mode 100644 (file)
index 0000000..e682f38
--- /dev/null
@@ -0,0 +1,209 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * File::Gettext
+ * 
+ * PHP versions 4 and 5
+ *
+ * @category   FileFormats
+ * @package    File_Gettext
+ * @author     Michael Wallner <mike@php.net>
+ * @copyright  2004-2005 Michael Wallner
+ * @license    BSD, revised
+ * @version    CVS: $Id: PO.php,v 1.6 2006/01/07 09:45:25 mike Exp $
+ * @link       http://pear.php.net/package/File_Gettext
+ */
+
+/**
+ * Requires File_Gettext
+ */
+require_once dirname(__FILE__) . '/../Gettext.php';
+
+/** 
+ * File_Gettext_PO
+ *
+ * GNU PO file reader and writer.
+ * 
+ * @author      Michael Wallner <mike@php.net>
+ * @version     $Revision: 1.6 $
+ * @access      public
+ */
+class File_Gettext_PO extends File_Gettext
+{
+    /**
+     * Constructor
+     *
+     * @access  public
+     * @return  object      File_Gettext_PO
+     * @param   string      path to GNU PO file
+     */
+    function File_Gettext_PO($file = '')
+    {
+        $this->file = $file;
+    }
+
+    /**
+     * Load PO file
+     *
+     * @access  public
+     * @return  mixed   Returns true on success or PEAR_Error on failure.
+     * @param   string  $file
+     */
+    function load($file = null)
+    {
+        $this->strings = array();
+        
+        if (!isset($file)) {
+            $file = $this->file;
+        }
+        
+        // load file
+        if (!$contents = @file($file)) {
+            return parent::raiseError($php_errormsg . ' ' . $file);
+        }
+       
+        $contents = explode("\n", implode('', $contents));
+       
+       $state = 0;
+       $comment = '';
+       
+       foreach($contents as $line) {
+           if( substr($line,0,1) == "#" ) {
+               $comment .= "$line\r\n";
+               continue;
+           }
+           
+           switch($state) {
+            case 0:
+               if( preg_match( '/^msgid\s+"(.*)"/', $line, $container ) ) {
+                   $state = 1;
+                   $msgid = $container[1];
+                   continue;
+               }
+               break;
+               
+            case 1:
+               if( preg_match( '/^msgstr\s+"(.*)"/', $line, $container ) ) {
+                   $msgstr = $container[1];
+                   $state = 2;
+               } else {
+                   $line = preg_replace( '/^"|"$/', "", $line );
+                   $msgid .= $line;
+               }
+               continue;
+               break;
+               
+            case 2:
+               if( preg_match( '/^\s*$/', $line ) ) {
+                   if( $msgid != "" ) {
+                       $this->status[parent::prepare($msgid)] = $this->status2array($comment);
+                       $this->ref[parent::prepare($msgid)] = $this->ref2array($comment);
+                       
+                       // $msgid = @preg_replace('/\s*msgid\s*"(.*)"\s*/s', '\\1', $matches[1][$i]);
+                       // $msgstr= @preg_replace('/\s*msgstr\s*"(.*)"\s*/s', '\\1', $matches[4][$i]);
+                       
+                       if( $msgstr == "" ) {
+                           $this->status[parent::prepare($msgid)][] = 'untranslated';
+                       } elseif (!in_array('fuzzy', $this->status[parent::prepare($msgid)])) {
+                           $this->status[parent::prepare($msgid)][] = 'translated';
+                       }
+
+                       $this->strings[parent::prepare($msgid)] = parent::prepare($msgstr);
+                       $this->encstr[base64_encode(parent::prepare($msgid))] = parent::prepare($msgid);
+                       $comment = preg_replace('/\r/', '', $comment);
+                       $this->comments[parent::prepare($msgid)] = $comment;
+
+                   } else {
+                       $this->meta = parent::meta2array(parent::prepare($msgstr));
+                   }
+                   $comment = "";
+                   $state = 0;
+               } else {
+                   $line = preg_replace( '/^"|"$/', "", $line );
+                   $msgstr .= $line;
+               }
+               break;
+           }
+       }
+
+        return true;
+    }
+
+    
+    function status2array($comment) {
+       $status = array();
+       $comment = explode("\n", $comment);
+       foreach($comment as $c) {
+           if (preg_match('/#,\s(.*)/', $c, $matches)) {
+               $st = preg_split('/,/', trim($matches[1]));
+               foreach($st as $s) {
+                   $status[] = trim($s);
+               }
+           }
+       }
+       return $status;
+    }
+    
+    function ref2array($comment) {
+       $refs = array();
+       $comment = explode("\n", $comment);
+       foreach($comment as $c) {
+           if (preg_match('/#:\s(.*)/', $c, $matches)) {
+               $st = preg_split('/\s/', trim($matches[1]));
+               foreach($st as $s) {
+                   $refs[] = trim($s);
+               }
+           }
+       }
+       return $refs;
+    }
+    
+    /**
+     * Save PO file
+     *
+     * @access  public
+     * @return  mixed   Returns true on success or PEAR_Error on failure.
+     * @param   string  $file
+     */
+    function save($file = null)
+    {
+        if (!isset($file)) {
+            $file = $this->file;
+        }
+        
+        // open PO file
+        if (!is_resource($fh = @fopen($file, 'w'))) {
+            return parent::raiseError($php_errormsg . ' ' . $file);
+        }
+        // lock PO file exclusively
+        if (!@flock($fh, LOCK_EX)) {
+            @fclose($fh);
+            return parent::raiseError($php_errmsg . ' ' . $file);
+        }
+        
+        // write meta info
+        if (count($this->meta)) {
+            $meta = 'msgid ""' . "\nmsgstr " . '""' . "\n";
+            foreach ($this->meta as $k => $v) {
+                $meta .= '"' . $k . ': ' . $v . '\n"' . "\n";
+            }
+            fwrite($fh, $meta . "\n");
+        }
+        // write strings
+        foreach ($this->strings as $o => $t) {
+           $c = @$this->comments[$o];
+            fwrite($fh,
+                $c . "\n" .
+                'msgid "'  . parent::prepare($o, true) . '"' . "\n" .
+                'msgstr "' . parent::prepare($t, true) . '"' . "\n\n"
+            );
+        }
+        
+        //done
+        @flock($fh, LOCK_UN);
+        @fclose($fh);
+        return true;
+    }
+}
+?>
diff --git a/babel/lib/Translate.php b/babel/lib/Translate.php
new file mode 100644 (file)
index 0000000..41727c3
--- /dev/null
@@ -0,0 +1,748 @@
+<?php
+/**
+ * Copyright 2000-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author  ???
+ * @package Babel
+ */
+
+class Translate {
+
+    function getPath($app) {
+       if ($app == 'horde') {
+           $app = '';
+       }
+       return realpath(HORDE_BASE . '/' . $app . '/po/');
+    }
+    
+    function stats($app, $filter_lang = false) {
+       global $nls, $module, $apps, $dirs, $lang;
+       
+       $report = array();
+       $dir = Translate::getPath($app);
+       
+       $i = 0;
+       $handle = opendir($dir);
+       while ($file = readdir($handle)) {
+           if (preg_match('/(.*)\.po$/', $file, $matches)) {
+               $locale = $matches[1];
+               if ($filter_lang && $locale != $filter_lang) {
+                   continue;
+               }
+               
+               if (!isset($nls['languages'][$locale]) || $locale == 'en_US') {
+                   continue; 
+               }
+               $i++;
+               
+               $pofile = $dir . "/$file";
+               
+               $tmppo = &new File_Gettext_PO();
+               $tmppo->load($pofile);
+               $fuzzy = 0;
+               $untranslated = 0;
+               $translated = 0;
+               $obsolete = 0;
+               
+               foreach($tmppo->status as $msgid => $status) {
+                   if (in_array('untranslated', $status)) {
+                       $untranslated++;
+                   } elseif (in_array('fuzzy', $status)) {
+                       $fuzzy++;
+                   } elseif (in_array('obsolete', $status)) {
+                       $obsolete++;
+                   } else {
+                       $translated++;
+                   }
+               }
+               
+               $all = $translated + $fuzzy + $untranslated;
+               $percent_done = round($translated / $all * 100, 2);
+               $report[$locale] = array($all, $percent_done, $translated, $fuzzy, $untranslated, $obsolete);
+           }
+       }
+       uasort ($report, 'my_usort_function');
+       
+       return $report;
+    }
+
+    function sanity_check()
+    {  
+       
+       /* Sanity checks */
+       if (!extension_loaded('gettext')) {
+           Translate_Display::error(_("Gettext extension not found!"));
+       }
+       
+       Translate_Display::info(_("Loading libraries..."));
+       $libs_found = true;
+       
+       foreach (array('Console_Getopt' => 'Console/Getopt.php',
+                      'Console_Table'  => 'Console/Table.php',
+                      'File_Find'      => 'File/Find.php')
+                as $class => $file) {
+           @include_once $file;
+           if (class_exists($class)) {
+               // Translate_Display::info("$class ...", false);
+           } else {
+               Translate_Display::error(sprintf(_("%s not found."), $class));
+               $libs_found = false;
+           }
+       }
+       
+       if (!$libs_found) {
+           Translate_Display::info();
+           Translate_Display::info(_("Make sure that you have PEAR installed and in your include path."));
+           Translate_Display::info('include_path: ' . ini_get('include_path'));
+       }
+    }
+    
+    function check_binaries()
+    {
+       global $gettext_version, $c;
+       
+       Translate_Display::info(_("Searching gettext binaries..."));
+       require_once 'System.php';
+       foreach (array('gettext', 'msgattrib', 'msgcat', 'msgcomm', 'msgfmt', 'msginit', 'msgmerge', 'xgettext') as $binary) {
+           $GLOBALS[$binary] = System::which($binary);
+           if ($GLOBALS[$binary]) {
+               // Translate_Display::info("$binary ... found: " . $GLOBALS[$binary], false);
+           } else {
+               Translate_Display::error(sprintf(_("%s not found."), $binary));
+           }
+       }
+       
+       $out = '';
+       exec($GLOBALS['gettext'] . ' --version', $out, $ret);
+       $split = explode(' ', $out[0]);
+       // Translate_Display::info('gettext version: ' . $split[count($split) - 1]);
+       $gettext_version = explode('.', $split[count($split) - 1]);
+       if ($gettext_version[0] == 0 && $gettext_version[1] < 12) {
+           $GLOBALS['php_support'] = false;
+           Translate_Display::info();
+           Translate_Display::warning(_("Warning: Your gettext version is too old and does not support PHP natively."));
+           Translate_Display::warning(_("Not all strings will be extracted."), false);
+       } else {
+           $GLOBALS['php_support'] = true;
+       }
+       Translate_Display::info();
+    }
+    
+    function search_file($file, $dir = '.', $local = false)
+    {
+       static $ff;
+       if (!isset($ff)) {
+           $ff = &new File_Find();
+       }
+       
+       if (substr($file, 0, 1) != '/') {
+           $file = "/$file/";
+       }
+       
+       if ($local) {
+           $files = $ff->glob($file, $dir, 'perl');
+           $files = array_map(create_function('$file', 'return "' . $dir . '/' . '" . $file;'), $files);
+           return $files;
+       } else {
+           return $ff->search($file, $dir, 'perl');
+       }
+    }
+    
+    function search_ext($ext, $dir = '.', $local = false)
+    {
+       return Translate::search_file(".+\\.$ext\$", $dir, $local);
+    }
+    
+    function get_po_files($dir)
+    {
+       $langs = Translate::search_ext('po', $dir);
+       if (($key = array_search($dir . '/' . 'messages.po', $langs)) !== false) {
+           unset($langs[$key]);
+       }
+       if (($key = array_search($dir . '/' . 'compendium.po', $langs)) !== false) {
+           unset($langs[$key]);
+       }
+       return $langs;
+    }
+    
+    function get_languages($dir)
+    {
+       chdir($dir);
+       $langs = get_po_files('po');
+       $langs = array_map(create_function('$lang', 'return str_replace("po" . '/', "", str_replace(".po", "", $lang));'), $langs);
+       return $langs;
+    }
+    
+    function search_applications()
+    {
+       $dirs = array();
+       $horde = false;
+       if (@is_dir(HORDE_BASE . '/' . 'po')) {
+           $dirs[] = HORDE_BASE;
+           $horde = true;
+       }
+       $dh = @opendir(HORDE_BASE);
+       if ($dh) {
+           while ($entry = @readdir($dh)) {
+               $dir = HORDE_BASE . '/' . $entry;
+               if (is_dir($dir) &&
+                   substr($entry, 0, 1) != '.' &&
+                   fileinode(HORDE_BASE) != fileinode($dir)) {
+                   $sub = opendir($dir);
+                   if ($sub) {
+                       while ($subentry = readdir($sub)) {
+                           if ($subentry == 'po' && is_dir($dir . '/' . $subentry)) {
+                               $dirs[] = $dir;
+                               if ($entry == 'horde') {
+                                   $horde = true;
+                               }
+                               break;
+                           }
+                       }
+                   }
+               }
+           }
+           if (!$horde) {
+               array_unshift($dirs, HORDE_BASE);
+           }
+       }
+       
+       return $dirs;
+    }
+    
+    function strip_horde($file)
+    {
+       if (is_array($file)) {
+           return array_map(create_function('$file', 'return Translate::strip_horde($file);'), $file);
+       } else {
+           return str_replace(HORDE_BASE . '/', '', $file);
+       }
+    }
+
+    function commit($help_only = false)
+    {
+       global $apps, $dirs, $lang, $module;
+       
+       $docs = false;
+       $files = array();
+       for ($i = 0; $i < count($dirs); $i++) {
+           if (!empty($module) && $module != $apps[$i]) continue;
+           if ($apps[$i] == 'horde') {
+               $dirs[] = $dirs[$i] . '/' . 'admin';
+               $apps[] = 'horde/admin';
+               if (!empty($module)) {
+                   $module = 'horde/admin';
+               }
+           }
+           if (empty($lang)) {
+               if ($help_only) {
+                   $files = array_merge($files, Translate::strip_horde(Translate::search_ext('xml', $dirs[$i] . '/' . 'locale')));
+               } else {
+                   $files = array_merge($files, Translate::strip_horde(Translate::get_po_files($dirs[$i] . '/' . 'po')));
+                   $files = array_merge($files, Translate::strip_horde(Translate::search_file('^[a-z]{2}_[A-Z]{2}', $dirs[$i] . '/' . 'locale', true)));
+               }
+           } else {
+               if ($help_only) {
+                   if (!@file_exists($dirs[$i] . '/' . 'locale' . '/' . $lang . '/' . 'help.xml')) continue;
+               } else {
+                   if (!@file_exists($dirs[$i] . '/po/' . $lang . '.po')) continue;
+                   $files[] = Translate::strip_horde($dirs[$i] . '/' . 'po' . '/' . $lang . '.po');
+               }
+               $files[] = Translate::strip_horde($dirs[$i] . '/' . 'locale' . '/' . $lang);
+           }
+           if ($docs && !$help_only && $apps[$i]) {
+               $files[] = Translate::strip_horde($dirs[$i] . '/' . 'docs');
+               if ($apps[$i] == 'horde') {
+                   $horde_conf = $dirs[array_search('horde', $dirs)] . '/' . 'config' . '/';
+                   $files[] = Translate::strip_horde($horde_conf . 'nls.php.dist');
+               }
+           }
+       }
+       chdir(HORDE_BASE);
+       if (count($files)) {
+           if ($docs) {
+               Translate_Display::info(_("Adding new files to repository:"));
+               $sh = 'cvs add';
+               foreach ($files as $file) {
+                   if (strstr($file, 'locale') || strstr($file, '.po')) {
+                       $sh .= " $file";
+                       Translate_Display::info($file, false);
+                   }
+               }
+               $sh .= '; cvs add';
+               foreach ($files as $file) {
+                   if (strstr($file, 'locale')) {
+                       if ($help_only) {
+                           $sh .= ' ' . $file . '/' . '*.xml';
+                           Translate_Display::info($file . '/' . '*.xml', false);
+                       } else {
+                           $sh .= ' ' . $file . '/' . '*.xml ' . $file . '/' . 'LC_MESSAGES';
+                           Translate_Display::info($file . '/' . "*.xml\n$file" . '/' . 'LC_MESSAGES', false);
+                       }
+                   }
+               }
+               if (!$help_only) {
+                   $sh .= '; cvs add';
+                   foreach ($files as $file) {
+                       if (strstr($file, 'locale')) {
+                           $add = $file . '/' . 'LC_MESSAGES' . '/' . '*.mo';
+                           $sh .= ' ' . $add;
+                           Translate_Display::info($add, false);
+                       }
+                   }
+               }
+               Translate_Display::info();
+               system($sh);
+               Translate_Display::info();
+           }
+           Translate_Display::header(_("Committing:"));
+           Translate_Display::info(implode(' ', $files), false);
+           if (!empty($lang)) {
+               $lang = ' ' . $lang;
+           }
+           if (empty($msg)) {
+               if ($docs) {
+                   $msg = "Add $lang translation.";
+               } elseif ($help_only) {
+                   $msg = "Update $lang help file.";
+               } else {
+                   $msg = "Update $lang translation.";
+               }
+           }
+           $sh = 'cvs commit -m "' . $msg . '" ' . implode(' ', $files);
+           system($sh);
+       }
+    }
+    
+    function xtract()
+    {
+       global $module, $apps, $dirs, $gettext_version;
+       
+       require_once 'Horde/Array.php';
+       if ($GLOBALS['php_support']) {
+           $language = 'PHP';
+       } else {
+           $language = 'C++';
+       }
+       for ($i = 0; $i < count($dirs); $i++) {
+           if (!empty($module) && $module != $apps[$i]) {
+               continue;
+           }
+           Translate_Display::header(sprintf(_("Extracting from %s... "), $apps[$i]));
+           chdir($dirs[$i]);
+           if ($apps[$i] == 'horde') {
+               $files = Translate::search_ext('php', '.', true);
+               foreach (array('admin', 'framework', 'lib', 'services', 'templates', 'util', 'config' . '/' . 'themes') as $search_dir) {
+                   $files = array_merge($files, Translate::search_ext('(php|inc|js)', $search_dir));
+               }
+               $files = array_merge($files, Translate::search_ext('(php|dist)', 'config'));
+               $sh = $GLOBALS['xgettext'] . ' --language=' . $language .
+                  ' --from-code=iso-8859-1 --keyword=_ --sort-output --copyright-holder="Horde Project"';
+               if ($gettext_version[0] > 0 || $gettext_version[1] > 11) {
+                   $sh .= ' --msgid-bugs-address="dev@lists.horde.org"';
+               }
+               $file = $dirs[$i] . '/' . 'po' . '/' . $apps[$i] . '.pot';
+               if (file_exists($file) && !is_writable($file)) {
+                   Translate_Display::error(sprintf(_('%s is not writable.', $file)));
+               }
+               $tmp_file = $file . '.tmp.pot';
+               $sh .= ' -o ' . $tmp_file . ' ' . implode(' ', $files);
+               if (@file_exists($dirs[$i] . '/po/translation.php')) {
+                   $sh .= ' po/translation.php';
+               }
+               exec($sh);
+           } else {
+               $files = Translate::search_ext('(php|inc|js)');
+               $files = array_filter($files, create_function('$file', 'return substr($file, 0, 9) != "./config/";'));
+               $files = array_merge($files, Translate::search_ext('(php|dist)', 'config'));
+               $sh = $GLOBALS['xgettext'] . ' --language=' . $language .
+                  ' --keyword=_ --sort-output --force-po --copyright-holder="Horde Project"';
+               if ($gettext_version[0] > 0 || $gettext_version[1] > 11) {
+                   $sh .= ' --msgid-bugs-address="support@scopserv.com"';
+               }
+               $file = 'po' . '/' . $apps[$i] . '.pot';
+               if (file_exists($file) && !is_writable($file)) {
+                   Translate_Display::error((sprintf(_("%s is not writable."), $file)));
+               }
+               $tmp_file = $file . '.tmp.pot';
+               $sh .= ' -o ' . $tmp_file . ' ' . implode(' ', $files);
+               exec($sh);
+           }
+           
+           if (file_exists($tmp_file)) {
+               $files = Translate::search_ext('html', 'templates');
+               $tmp = fopen($file . '.templates', 'w');
+               foreach ($files as $template) {
+                   $fp = fopen($template, 'r');
+                   $lineno = 0;
+                   while (($line = fgets($fp, 4096)) !== false) {
+                       $lineno++;
+                       $offset = 0;
+                       while (($left = strpos($line, '<gettext>', $offset)) !== false) {
+                           $left += 9;
+                           $buffer = '';
+                           $linespan = 0;
+                           while (($end = strpos($line, '</gettext>', $left)) === false) {
+                               $buffer .= substr($line, $left);
+                               $left = 0;
+                               $line = fgets($fp, 4096);
+                               $linespan++;
+                               if ($line === false) {
+                                   Translate_Display::error((sprintf(_("<gettext> tag not closed in file %s.\nOpening tag found in line %d."), $template, $lineno)));
+                                   break 2;
+                               }
+                           }
+                           $buffer .= substr($line, $left, $end - $left);
+                           fwrite($tmp, "#: $template:$lineno\n");
+                           fwrite($tmp, 'msgid "' . str_replace(array('"', "\n"), array('\"', "\\n\"\n\""), $buffer) . "\"\n");
+                           fwrite($tmp, 'msgstr ""' . "\n\n");
+                           
+                           $offset = $end + 10;
+                       }
+                   }
+                   fclose($fp);
+               }
+               fclose($tmp);
+               $sh = $GLOBALS['msgcomm'] . " --more-than=0 --sort-output \"$tmp_file\" \"$file.templates\" --output-file \"$tmp_file\"";
+               exec($sh);
+               unlink($file . '.templates');
+               
+               if (file_exists($file)) {
+                   $diff = array_diff(file($tmp_file), file($file));
+                   $diff = preg_grep('/^"POT-Creation-Date:/', $diff, PREG_GREP_INVERT);
+               }
+           }
+           
+           if (!file_exists($file) || count($diff)) {
+               @unlink($file);
+               rename($tmp_file, $file);
+               Translate_Display::info(_("Updated!"));
+           } else {
+               @unlink($tmp_file);
+               Translate_Display::info(_("Not changed!"));
+           }
+       }
+    }
+    
+    function merge()
+    {
+       global $apps, $dirs, $lang, $module;
+       
+       $compendium = ' --compendium="' . HORDE_BASE . '/' . 'po' . '/' . 'compendium.po"';
+       // $compendium = ' --compendium=' . $option[1];
+       // $compendium = '';
+       
+       if (!isset($lang) && !empty($compendium)) {
+           Translate_Display::error(_("Error: No locale specified."));
+           Translate_Display::info();
+           usage();
+       }
+       
+       Translate::cleanup();
+       
+       for ($i = 0; $i < count($dirs); $i++) {
+           if (!empty($module) && $module != $apps[$i]) {
+               continue;
+           }
+           Translate_Display::header(sprintf(_("Merging translation for module %s..."), $apps[$i]));
+           $dir = $dirs[$i] . '/' . 'po' . '/';
+           if (empty($lang)) {
+               $langs = get_languages($dirs[$i]);
+           } else {
+               if (!@file_exists($dir . $lang . '.po')) {
+                   Translate_Display::info(_("Skipped..."));
+                   Translate_Display::info();
+                   continue;
+               }
+               $langs = array($lang);
+           }
+           foreach ($langs as $locale) {
+               Translate_Display::info(sprintf(_("Merging locale %s..."), $locale));
+               $sh = $GLOBALS['msgmerge'] . ' --update -v' . $compendium . ' "' . $dir . $locale . '.po" "' . $dir . $apps[$i] . '.pot"';
+               exec($sh);
+               Translate_Display::info(_("Done!"));
+           }
+       }
+    }
+    
+    function compendium()
+    {
+       global $dirs, $lang, $module;
+       
+       $dir = HORDE_BASE . '/' . 'po' . '/';
+       $add = '';
+       if (!isset($lang)) {
+           Translate_Display::error(_("Error: No locale specified."));
+           Translate_Display::info();
+           usage();
+       }
+       Translate_Display::info(sprintf(_("Merging all %s.po files to the compendium... "), $lang));
+       $pofiles = array();
+       for ($i = 0; $i < count($dirs); $i++) {
+           $pofile = $dirs[$i] . '/' . 'po' . '/' . $lang . '.po';
+           if (file_exists($pofile)) {
+               $pofiles[] = $pofile;
+           }
+       }
+       if (!empty($dir) && substr($dir, -1) != '/') {
+           $dir .= '/';
+       }
+       $sh = $GLOBALS['msgcat'] . ' --sort-output ' . implode(' ', $pofiles) . $add . ' > ' . $dir . 'compendium.po ';
+       exec($sh, $out, $ret);
+       
+       if ($ret == 0) {
+           Translate_Display::info(_("Done!"));
+       } else {
+           Translate_Display::error(_("Failed!"));
+       }
+    }
+    
+    function init()
+    {
+       global $module, $apps, $dirs, $lang, $module;
+       
+       if (empty($lang)) { $lang = getenv('LANG'); }
+       for ($i = 0; $i < count($dirs); $i++) {
+           if (@file_exists($dirs[$i] . '/po/' . $lang . '.po')) {
+               continue;
+           }
+           if (!empty($module) && $module != $apps[$i]) { continue; }
+           $package = ucfirst($apps[$i]);
+           $package_u = String::upper($apps[$i]);
+           @include $dirs[$i] . '/lib/version.php';
+           $version = eval('return(defined("' . $package_u . '_VERSION") ? ' . $package_u . '_VERSION : "???");');
+           Translate_Display::header(sprintf(_("Initializing module %s..."), $apps[$i]));
+           if (!@file_exists($dirs[$i] . '/po/' . $apps[$i] . '.pot')) {
+               Translate_Display::error(_("Failed!"));
+               Translate_Display::info(sprintf(_("%s not found. Run 'Extract' first."), $dirs[$i] . '/' . 'po' . '/' . $apps[$i] . '.pot'));
+               continue;
+           }
+           $dir = $dirs[$i] . '/' . 'po' . '/';
+           $sh = $GLOBALS['msginit'] . ' --no-translator -i ' . $dir . $apps[$i] . '.pot ' .
+              (!empty($lang) ? ' -o ' . $dir . $lang . '.po --locale=' . $lang : '');
+           
+           if (!empty($lang) && !OS_WINDOWS) {
+               $pofile = $dirs[$i] . '/po/' . $lang . '.po';
+               $sh .= "; sed 's/PACKAGE package/$package package/' $pofile " .
+                 "| sed 's/PACKAGE VERSION/$package $version/' " .
+                 "| sed 's/messages for PACKAGE/messages for $package/' " .
+                 "| sed 's/Language-Team: none/Language-Team: i18n@lists.horde.org/' " .
+                 "> $pofile.tmp";
+           }
+           exec($sh, $out, $ret);
+           rename($pofile . '.tmp', $pofile);
+           if ($ret == 0) {
+               Translate_Display::info(_("Done!"));
+           } else {
+               Translate_Display::error(_("Failed!"));
+           }
+           Translate_Display::info();
+       }
+    }
+    
+    function make()
+    {
+       global $apps, $dirs, $lang, $module;
+       
+       $compendium = HORDE_BASE . '/' . 'po' . '/' . 'compendium.po';
+       $save_stats = true;
+       
+       $horde = array_search('horde', $dirs);
+       $horde_msg = array();
+       $stats_array = array();
+       
+       for ($i = 0; $i < count($dirs); $i++) {
+           if (!empty($module) && $module != $apps[$i]) continue;
+           Translate_Display::header(sprintf(_("Building MO files for module %s..."), $apps[$i]));
+           if (empty($lang)) {
+               $langs = get_languages($dirs[$i]);
+           } else {
+               if (!@file_exists($dirs[$i] . '/' . 'po' . '/' . $lang . '.po')) {
+                   Translate_Display::info(_("Skipped..."));
+                   Translate_Display::info();
+                   continue;
+               }
+               $langs = array($lang);
+           }
+           foreach ($langs as $locale) {
+               Translate_Display::info(sprintf(_("Building locale %s..."), $locale));
+               $dir = $dirs[$i] . '/' . 'locale' . '/' . $locale . '/' . 'LC_MESSAGES';
+               if (!is_dir($dir)) {
+                   require_once 'System.php';
+                   if (!@System::mkdir("-p $dir")) {
+                       Translate_Display::error(sprintf(_("Warning: Could not create locale directory for locale %s:"), $locale));
+                       Translate_Display::info($dir, false);
+                       Translate_Display::info();
+                       continue;
+                   }
+               }
+               
+               /* Convert to unix linebreaks. */
+               $pofile = $dirs[$i] . '/' . 'po' . '/' . $locale . '.po';
+               $fp = fopen($pofile, 'r');
+               $content = fread($fp, filesize($pofile));
+               fclose($fp);
+               
+               $content = str_replace("\r", '', $content);
+               $fp = fopen($pofile, 'wb');
+               fwrite($fp, $content);
+               fclose($fp);
+               
+               /* Check PO file sanity. */
+               $sh = $GLOBALS['msgfmt'] . " --check \"$pofile\" 2>&1";
+               exec($sh, $out, $ret);
+               if ($ret != 0) {
+                   Translate_Display::error(_("Warning: an error has occured:"));
+                   Translate_Display::info(implode("\n", $out));
+                   Translate_Display::info();
+                   if ($apps[$i] == 'horde') {
+                       continue 2;
+                   }
+                   continue;
+               }
+               
+               /* Compile MO file. */
+               $sh = $GLOBALS['msgfmt'] . ' --statistics -o "' . $dir . '/' . $apps[$i] . '.mo"';
+               if ($apps[$i] != 'horde') {
+                   $horde_po = $dirs[$horde] . '/' . 'po' . '/' . $locale . '.po';
+                   if (!@is_readable($horde_po)) {
+                       Translate_Display::error(sprintf(_("Warning: the Horde PO file for the locale %s does not exist:"), $locale));
+                       Translate_Display::info($horde_po);
+                       Translate_Display::info();
+                       $sh .= $dirs[$i] . '/' . 'po' . '/' . $locale . '.po';
+                   } else {
+                       $sh = "export LANG=C ; " . $GLOBALS['msgcomm'] . " --more-than=0 --sort-output \"$pofile\" \"$horde_po\" | $sh -";
+                   }
+               } else {
+                   $sh .= $pofile;
+               }
+               $sh .= ' 2>&1';
+               $out = '';
+
+               exec($sh, $out, $ret);
+               
+               if ($ret == 0) {
+                   Translate_Display::info(_("Done!"));
+                   $messages = array(0, 0, 0);
+                   if (preg_match('/(\d+) translated/', $out[0], $match)) {
+                       $messages[0] = $match[1];
+                       if (isset($horde_msg[$locale])) {
+                           $messages[0] -= $horde_msg[$locale][0];
+                           if ($messages[0] < 0) $messages[0] = 0;
+                       }
+                   }
+                   if (preg_match('/(\d+) fuzzy/', $out[0], $match)) {
+                       $messages[1] = $match[1];
+                       if (isset($horde_msg[$locale])) {
+                           $messages[1] -= $horde_msg[$locale][1];
+                           if ($messages[1] < 0) $messages[1] = 0;
+                       }
+                   }
+                   if (preg_match('/(\d+) untranslated/', $out[0], $match)) {
+                       $messages[2] = $match[1];
+                       if (isset($horde_msg[$locale])) {
+                           $messages[2] -= $horde_msg[$locale][2];
+                           if ($messages[2] < 0) $messages[2] = 0;
+                       }
+                   }
+                   if ($apps[$i] == 'horde') {
+                       $horde_msg[$locale] = $messages;
+                   }
+                   $stats_array[$apps[$i]][$locale] = $messages;
+               } else {
+                   Translate_Display::error(_("Failed!"));
+                   exec($sh, $out, $ret);
+                   Translate_Display::info(implode("\n", $out));
+               }
+               if (count($langs) > 1) {
+                   continue;
+               }
+               
+               /* Merge translation into compendium. */
+               if (!empty($compendium)) {
+                   Translate_Display::header(sprintf(_("Merging the PO file for %s to the compendium..."), $apps[$i]));
+                   if (!empty($dir) && substr($dir, -1) != '/') {
+                       $dir .= '/';
+                   }
+                   $sh = $GLOBALS['msgcat'] . " --sort-output \"$compendium\" \"$pofile\" > \"$compendium.tmp\"";
+                   $out = '';
+                   exec($sh, $out, $ret);
+                   @unlink($compendium);
+                   rename($compendium . '.tmp', $compendium);
+                   if ($ret == 0) {
+                       Translate_Display::info(_("Done!"));
+                   } else {
+                       Translate_Display::error(_("Failed!"));
+                   }
+               }
+               Translate_Display::info();
+           }
+       }
+       if (empty($module)) {
+           Translate_Display::header(_("Results:"));
+       } else {
+           Translate_Display::header(_("Results (including Horde):"));
+       }
+       
+       echo '<br />';
+       echo '<table cellspacing=0 cellpadding=0 width=90%>';
+       echo sprintf('<tr><td class="control" width="15%%"><b>%s</b></td><td class="control"><b>%s</b></td><td align="right"  width="5%%" class="control"><b>%s</b></td><td align="right"  width="5%%" class="control"><b>%s</b></td><td align="right"  width="5%%" class="control"><b>%s</b></td></tr>', 'Module', 'Language', 'Translated', 'Fuzzy', 'Untranslated');
+       
+       $i = 0;
+       foreach($stats_array as $app => $info) {
+           foreach($info as $locale => $message) {
+               echo sprintf('<tr class="item%d"><td>%s</td><td>%s</td><td align="right" >%s</td><td align="right" >%s</td><td align="right" >%s</td></tr>',
+                            ($i++ %2), $app, $locale, $messages[0], $messages[1], $messages[2]);
+           }
+       }
+       echo '</table>';
+       
+       if ($save_stats) {
+           $fp = @fopen('/tmp/translation_stats.txt', 'w');
+           if ($fp) {
+               fwrite($fp, serialize($stats_array));
+               fclose($fp);
+           }
+       }
+    }
+    
+    function cleanup($keep_untranslated = false)
+    {
+       global $apps, $dirs, $lang, $module;
+       
+       for ($i = 0; $i < count($dirs); $i++) {
+           if (!empty($module) && $module != $apps[$i]) { continue; }
+           Translate_Display::header(sprintf(_("Cleaning up PO files for module %s..."), $apps[$i]));
+           if (empty($lang)) {
+               $langs = get_languages($dirs[$i]);
+           } else {
+               if (!@file_exists($dirs[$i] . '/' . 'po' . '/' . $lang . '.po')) {
+                   Translate_Display::info(_("Skipped..."));
+                   Translate_Display::info();
+                   continue;
+               }
+               $langs = array($lang);
+           }
+           foreach ($langs as $locale) {
+               Translate_Display::info(sprintf(_("Cleaning up locale %s..."), $locale));
+               $pofile = $dirs[$i] . '/' . 'po' . '/' . $locale . '.po';
+               $sh = $GLOBALS['msgattrib'] . ($keep_untranslated ? '' : ' --translated') . " --no-obsolete --no-fuzzy --force-po $pofile > $pofile.tmp";
+               $out = '';
+               exec($sh, $out, $ret);
+               if ($ret == 0) {
+                   @unlink($pofile);
+                   rename($pofile . '.tmp', $pofile);
+                   Translate_Display::info(_("Done!"));
+               } else {
+                   @unlink($pofile . '.tmp', $pofile);
+                   Translate_Display::error(_("Failed!"));
+               }
+               Translate_Display::info();
+           }
+       }
+    }
+}
+
diff --git a/babel/lib/Translate_Help.php b/babel/lib/Translate_Help.php
new file mode 100644 (file)
index 0000000..b401a63
--- /dev/null
@@ -0,0 +1,238 @@
+<?php
+/**
+ * Copyright 2000-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author  ???
+ * @package Babel
+ */
+
+class Translate_Help {
+    function update_help()
+    {
+       global $dirs, $apps, $last_error_msg, $lang, $module;
+       
+       $files = array();
+       for ($i = 0; $i < count($dirs); $i++) {
+           if (!empty($module) && $module != $apps[$i]) { continue; }
+           if (!is_dir("$dirs[$i]/locale")) continue;
+           if ($apps[$i] == 'horde') {
+               $dirs[] = $dirs[$i] . '/' . 'admin';
+               $apps[] = 'horde/admin';
+               if (!empty($module)) {
+                   $module = 'horde/admin';
+               }
+           }
+           if (empty($lang)) {
+               $files = search_file('help.xml', $dirs[$i] . '/' . 'locale');
+           } else {
+               $files = array($dirs[$i] . '/' . 'locale' . '/' . $lang . '/' . 'help.xml');
+           }
+           $file_en  = $dirs[$i] . '/' . 'locale' . '/' . 'en_US' . '/' . 'help.xml';
+           if (!@file_exists($file_en)) {
+               Translate_Display::info(sprintf(_("Warning: There doesn't yet exist a help file for %s."), $apps[$i]));
+               Translate_Display::info();
+               continue;
+           }
+           foreach ($files as $file_loc) {
+               $locale = substr($file_loc, 0, strrpos($file_loc, '/'));
+               $locale = substr($locale, strrpos($locale, '/') + 1);
+               if ($locale == 'en_US') continue;
+               if (!@file_exists($file_loc)) {
+                   Translate_Display::info(sprintf(_("Warning: The %s help file for %s doesn't yet exist. Creating a new one."), $locale, $apps[$i]));
+                   $dir_loc = substr($file_loc, 0, -9);
+                   if (!is_dir($dir_loc)) {
+                       require_once 'System.php';
+                       if (!@System::mkdir("-p $dir_loc")) {
+                           Translate_Display::error(sprintf(_("Warning: Could not create locale directory for locale %s:"), $locale));
+                           Translate_Display::info($dir_loc, false);
+                           Translate_Display::info();
+                           continue;
+                       }
+                   }
+                   
+                   if (!@copy($file_en, $file_loc)) {
+                       Translate_Display::error(sprintf(_("Warning: Could not copy %s to %s"), $file_en, $file_loc));
+                   }
+                   Translate_Display::info();
+                   continue;
+               }
+               Translate_Display::info(sprintf(_("Updating %s help file for %s."), $locale, $apps[$i]));
+               $fp = fopen($file_loc, 'r');
+               $line = fgets($fp);
+               fclose($fp);
+               if (!strstr($line, '<?xml')) {
+                   Translate_Display::info(sprintf(_("Warning: The help file %s didn't start with &lt;?xml"), $file_loc));
+                   Translate_Display::info();
+                   continue;
+               }
+               $encoding = '';
+               if (preg_match('/encoding=(["\'])([^\\1]+)\\1/', $line, $match)) {
+                   $encoding = $match[2];
+               }
+               $doc_en = domxml_open_file($file_en);
+               if (!is_object($doc_en)) {
+                   Translate_Display::info(sprintf(_("Warning: There was an error opening the file %s."), $file_en));
+                   Translate_Display::info();
+                   continue 2;
+               }
+               $doc_loc = domxml_open_file($file_loc);
+               if (!is_object($doc_loc)) {
+                   Translate_Display::info(sprintf(_("There was an error opening the file %s."), $file_loc));
+                   Translate_Display::info();
+                   continue;
+               }
+               $doc_new  = domxml_new_doc('1.0');
+               $help_en  = $doc_en->document_element();
+               $help_loc = $doc_loc->document_element();
+               $help_new = $help_loc->clone_node();
+               $entries_loc = array();
+               $entries_new = array();
+               $count_uptodate = 0;
+               $count_new      = 0;
+               $count_changed  = 0;
+               $count_unknown  = 0;
+               foreach ($doc_loc->get_elements_by_tagname('entry') as $entry) {
+                   $entries_loc[$entry->get_attribute('id')] = $entry;
+               }
+               foreach ($doc_en->get_elements_by_tagname('entry') as $entry) {
+                   $id = $entry->get_attribute('id');
+                   if (array_key_exists($id, $entries_loc)) {
+                       if ($entries_loc[$id]->has_attribute('md5') &&
+                           md5($entry->get_content()) != $entries_loc[$id]->get_attribute('md5')) {
+                           $comment = $doc_loc->create_comment(" English entry:\n" . str_replace('--', '&#45;&#45;', $doc_loc->dump_node($entry)));
+                           $entries_loc[$id]->append_child($comment);
+                           $entry_new = $entries_loc[$id]->clone_node(true);
+                           $entry_new->set_attribute('state', 'changed');
+                           $count_changed++;
+                       } else {
+                           if (!$entries_loc[$id]->has_attribute('state')) {
+                               $comment = $doc_loc->create_comment(" English entry:\n" . str_replace('--', '&#45;&#45;', $doc_loc->dump_node($entry)));
+                               $entries_loc[$id]->append_child($comment);
+                               $entry_new = $entries_loc[$id]->clone_node(true);
+                               $entry_new->set_attribute('state', 'unknown');
+                               $count_unknown++;
+                           } else {
+                               $entry_new = $entries_loc[$id]->clone_node(true);
+                               $count_uptodate++;
+                           }
+                       }
+                   } else {
+                       $entry_new = $entry->clone_node(true);
+                       $entry_new->set_attribute('state', 'new');
+                       $count_new++;
+                   }
+                   $entries_new[] = $entry_new;
+               }
+               $doc_new->append_child($doc_new->create_comment(' $' . 'Horde$ '));
+               foreach ($entries_new as $entry) {
+                   $help_new->append_child($entry);
+               }
+               Translate_Display::info(sprintf(_("Entries: %d total, %d up-to-date, %d new, %d changed, %d unknown"),
+                                               $count_uptodate + $count_new + $count_changed + $count_unknown,
+                                               $count_uptodate, $count_new, $count_changed, $count_unknown), false);
+               $doc_new->append_child($help_new);
+               $output = $doc_new->dump_mem(true, $encoding);
+               $fp = fopen($file_loc, 'w');
+               $line = fwrite($fp, $output);
+               fclose($fp);
+               Translate_Display::info(sprintf(_("%d bytes written."), strlen($output)), false);
+               Translate_Display::info();
+           }
+       }
+    }
+    
+    function make_help()
+    {
+       global $dirs, $apps, $lang, $module;
+       
+       $files = array();
+       for ($i = 0; $i < count($dirs); $i++) {
+           if (!empty($module) && $module != $apps[$i]) continue;
+           if (!is_dir("$dirs[$i]/locale")) continue;
+           if ($apps[$i] == 'horde') {
+               $dirs[] = $dirs[$i] . '/' . 'admin';
+               $apps[] = 'horde/admin';
+               if (!empty($module)) {
+                   $module = 'horde/admin';
+               }
+           }
+           if (empty($lang)) {
+               $files = search_file('help.xml', $dirs[$i] . '/' . 'locale');
+           } else {
+               $files = array($dirs[$i] . '/' . 'locale' . '/' . $lang . '/' . 'help.xml');
+           }
+           $file_en  = $dirs[$i] . '/' . 'locale' . '/' . 'en_US' . '/' . 'help.xml';
+           if (!@file_exists($file_en)) {
+               continue;
+           }
+           foreach ($files as $file_loc) {
+               if (!@file_exists($file_loc)) {
+                   Translate_Display::info(_("Skipped..."));
+                   Translate_Display::info();
+                   continue;
+               }
+               $locale = substr($file_loc, 0, strrpos($file_loc, '/'));
+               $locale = substr($locale, strrpos($locale, '/') + 1);
+               if ($locale == 'en_US') continue;
+               Translate_Display::info(sprintf(_("Updating %s help file for %s."), ($locale), ($apps[$i])));
+               $fp = fopen($file_loc, 'r');
+               $line = fgets($fp);
+               fclose($fp);
+               if (!strstr($line, '<?xml')) {
+                   Translate_Display::info(sprintf(_("Warning: The help file %s didn't start with &lt;?xml"), $file_loc));
+                   Translate_Display::info();
+                   continue;
+               }
+               $encoding = '';
+               if (preg_match('/encoding=(["\'])([^\\1]+)\\1/', $line, $match)) {
+                   $encoding = $match[2];
+               }
+               $doc_en   = domxml_open_file($file_en);
+               if (!is_object($doc_en)) {
+                   Translate_Display::info(sprintf(_("Warning: There was an error opening the file %s."), $file_en));
+                   Translate_Display::info();
+                   continue 2;
+               }
+               $doc_loc  = domxml_open_file($file_loc);
+               if (!is_object($doc_loc)) {
+                   Translate_Display::info(sprintf(_("Warning: There was an error opening the file %s."), $file_loc));
+                   Translate_Display::info();
+                   continue;
+               }
+               $help_loc = $doc_loc->document_element();
+               $md5_en   = array();
+               $count_all = 0;
+               $count     = 0;
+               foreach ($doc_en->get_elements_by_tagname('entry') as $entry) {
+                   $md5_en[$entry->get_attribute('id')] = md5($entry->get_content());
+               }
+               foreach ($doc_loc->get_elements_by_tagname('entry') as $entry) {
+                   foreach ($entry->child_nodes() as $child) {
+                       if ($child->node_type() == XML_COMMENT_NODE && strstr($child->node_value(), 'English entry')) {
+                           $entry->remove_child($child);
+                       }
+                   }
+                   $count_all++;
+                   $id = $entry->get_attribute('id');
+                   if (!array_key_exists($id, $md5_en)) {
+                       Translate_Display::info(sprintf(_("No entry with the id '%s' exists in the original help file."), $id));
+                   } else {
+                       $entry->set_attribute('md5', $md5_en[$id]);
+                       $entry->set_attribute('state', 'uptodate');
+                       $count++;
+                   }
+               }
+               $output = $doc_loc->dump_mem(true, $encoding);
+               $fp = fopen($file_loc, 'w');
+               $line = fwrite($fp, $output);
+               fclose($fp);
+               
+               Translate_Display::info(sprintf(_("%d of %d entries marked as up-to-date"), $count, $count_all), false);
+               Translate_Display::info();
+           }
+       }
+    }
+}
diff --git a/babel/lib/Translation.php b/babel/lib/Translation.php
new file mode 100644 (file)
index 0000000..e3adbe9
--- /dev/null
@@ -0,0 +1,312 @@
+<?php
+/**
+ * Copyright 2000-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author  Joel Vandal <joel@scopserv.com>
+ * @package Babel
+ */
+
+class Translation {
+
+    function callHook($fname, $info) {
+       /* Check if an hooks file exist */
+       if (file_exists(BABEL_BASE . '/config/hooks.php')) {
+           include_once BABEL_BASE . '/config/hooks.php';
+           
+           $func = '_translation_hook_' . $fname;
+           
+           if (function_exists($func)) {
+               $res = call_user_func($func, $info);
+           } else {
+               Translate_Display::warning(sprintf(_("Function doesn't exist: %s"), $func));
+           }
+       } else {
+           Translate_Display::warning(_("Hook file doesn't exist"));
+       }
+    }
+    
+    function displayLanguage() {
+       global $nls, $lang, $app;
+       
+       if (!isset($nls['languages'][$lang])) {
+           return;
+       }
+       
+       $res = sprintf(_("Language: %s (%s)"), $nls['languages'][$lang], $lang);
+       if ($app) {
+           $res .= '&nbsp; | &nbsp; ' . sprintf(_("Module: %s"), $app);
+       }
+       
+       return $res;
+    }
+
+    
+    function ModuleSelection() {
+       $html = '';
+       $html .= '<span style="float:right">';
+       $html .= '<form action="' . Horde::selfUrl() . '" method="post" name="moduleSelector">';
+       $html .= '<select name="module" onchange="moduleSubmit()">';
+       
+       $apps = array('ALL' => _("All Applications")) +  Translation::listApps();
+       
+       foreach($apps as $app => $desc) {
+           if (!Translation::hasPermission("module:$app")) {
+               continue;
+           }
+           
+           if (Util::getFormData('module') == $app) {
+               $html .= '<option class="control" value="' . $app . '" selected>' .  '+ ' . $desc;
+           } else {
+               $html .= '<option value="' . $app . '">' . '&#8211; ' . $desc;
+           }
+       }
+       
+       $html .= '</select>';
+       $html .= '</form>';
+       $html .= '</span>';
+       
+       $html .= '<script language="JavaScript" type="text/javascript">' . "\n";
+       $html .= '<!--' . "\n";
+       $html .= 'var loading;' . "\n";
+       $html .= 'function moduleSubmit()' . "\n";
+       $html .= '{' . "\n";
+       $html .= 'document.moduleSelector.submit();' . "\n";
+       $html .= 'return false;' . "\n";
+       $html .= '}' . "\n";
+       $html .= '// -->' . "\n";
+       $html .= '</script>' . "\n";
+       return $html;
+    }
+    
+    function LanguageSelection() {
+       global $nls, $app;
+       
+       $html = '';
+       $html .= '<span style="float:right">';
+       $html .= '<form action="' . Horde::selfUrl() . '" method="post" name="languageSelector">';
+       $html .= '&nbsp;';
+       $html .= '<input type="hidden" name="module" value="' . $app . '">';
+       $html .= '<select name="display_language" onchange="languageSubmit()">';
+       
+       $tests =  $nls['languages'];
+       
+       // Unset English
+       unset($tests['en_US']);
+       
+       foreach($tests as $dir => $desc) {
+           if (!Translation::hasPermission("language:$dir")) {
+               continue;
+           }
+           
+           if (isset($_SESSION['translation']['language']) && $dir == $_SESSION['translation']['language']) {
+               $html .= '<option class="control" value="' . $dir . '" selected>' .  '+ ' . $desc;
+           } else {
+               $html .= '<option value="' . $dir . '">' . '&#8211; ' . $desc;
+           }
+       }
+       
+       $html .= '</select>';
+       $html .= '&nbsp;';
+       $html .= '</form>';
+       $html .= '</span>';
+       
+       $html .= '<script language="JavaScript" type="text/javascript">' . "\n";
+       $html .= '<!--' . "\n";
+       $html .= 'var loading;' . "\n";
+       $html .= 'function languageSubmit()' . "\n";
+       $html .= '{' . "\n";
+       $html .= 'document.languageSelector.submit();' . "\n";
+       $html .= 'return false;' . "\n";
+       $html .= '}' . "\n";
+       $html .= '// -->' . "\n";
+       $html .= '</script>' . "\n";
+       return $html;
+    }
+    
+    function listApps($all = false) {
+       global $registry;
+
+       $res = array();
+       
+       if ($all) {
+           $res['ALL'] = _("All Applications");
+       }
+       
+       foreach ($registry->applications as $app => $params) {
+           if ($params['status'] == 'heading' || $params['status'] == 'block') {
+               continue;
+           }
+           
+           if (isset($params['fileroot']) && !is_dir($params['fileroot'])) {
+               continue;
+           }
+           
+           if (preg_match('/_reports$/', $app) || preg_match('/_tools$/', $app)) {
+               continue;
+           }
+           
+           if (Translation::hasPermission("module:$app")) {
+               $res[$app] = sprintf("%s (%s)", $params['name'], $app);
+           }
+       }
+       return $res;
+    }
+      
+    /**
+     * Returns the value of the specified permission for $userId.
+     *
+     * @return mixed  Does user have $permission?
+     */
+    function hasPermission($permission, $filter = null, $perm = null)
+    {
+       global $perms;
+       
+       $userId = Auth::getAuth();
+       $admin = ($userId == 'admin') ? true : false;
+       
+       if ($admin || !$perms->exists('translation:' . $permission)) {
+           return true;
+       }
+       
+       $allowed = $perms->getPermissions('translation:' . $permission);
+       
+       switch ($filter) {
+        case 'tabs':
+           if ($perm) {
+               $allowed  = $perms->hasPermission('translation:' . $permission, Auth::getAuth(), $perm);
+           }
+           break;
+       }
+       return $allowed;
+    }
+
+    /**
+     * Get the module main Menu.
+     **/
+    function getMenu($returnType = 'object')
+    {
+        global $registry;
+
+       require_once 'Horde/Menu.php';
+       $menu = &new Menu();
+       
+        $menu->addArray(array('url' => Horde::applicationUrl('index.php'),
+                             'text' => _("_General"),
+                             'icon' => 'list.png'));
+
+       if (Translation::hasPermission('view')) {
+           $menu->addArray(array('url' => Horde::applicationUrl('view.php'),
+                                 'text' => _("_View"),
+                                 'icon' => 'view.png'));
+       }
+       
+       if (Translation::hasPermission('stats')) {
+           $menu->addArray(array('url' => Horde::applicationUrl('stats.php'),
+                                 'text' => _("_Stats"),
+                                 'icon' => 'extract.png'));
+       }
+       
+       if (Translation::hasPermission('extract')) {
+           $menu->addArray(array('url' => Horde::applicationUrl('extract.php'),
+                                 'text' => _("_Extract"),
+                                 'icon' => 'extract.png'));
+       }
+       
+       if (Translation::hasPermission('make')) {
+           $menu->addArray(array('url' => Horde::applicationUrl('make.php'),
+                                 'text' => _("_Make"),
+                                 'icon' => 'make.png'));
+       }
+
+       if (Translation::hasPermission('upload')) {
+           $menu->addArray(array('url' => Horde::applicationUrl('upload.php'),
+                                 'text' => _("_Upload"),
+                                 'icon' => 'upload.png'));
+       }
+       if ($returnType == 'object') {
+           return $menu;
+       } else {
+           return $menu->render();
+       }
+    }
+
+
+    /**
+     * Send an Email.
+     **/
+    function sendEmail($email, $type = 'html', $attachments = array()) {
+       global $client, $scopserv;
+       
+       include_once("Mail.php");
+       include_once("Mail/mime.php");
+
+       $headers["From"]    = $email['from'];
+       $headers["Subject"] = $email['subject'];
+       
+       $mime = new Mail_Mime();
+       if ($type == 'html') {
+           $mime->setHtmlBody($email['content']);
+       } else {
+           $mime->setTxtBody($email['content']);
+       }
+       
+       if (!empty($attachments)) {
+           foreach ($attachments as $info) {
+               $mime->addAttachment($info['file'],
+                                    $info['type'],
+                                    $info['name'], false);
+           }
+       }
+       
+       $body = $mime->get();
+       $hdrs = $mime->headers($headers);
+       
+       $mail_object = &Mail::factory("mail");
+       return $mail_object->send($email['to'], $hdrs, $body);
+    }
+
+
+    function RB_init() {
+       Horde::addScriptFile('prototype.js', 'translation', true);
+       Horde::addScriptFile('effects.js', 'translation', true);
+       Horde::addScriptFile('redbox.js', 'translation', true);
+    }
+    
+    function RB_start($secs = 30) {
+       
+       $msg = '';
+       $msg .= '<table width=100% id="RB_confirm"><tr><td>';
+       $msg .= '<b>' . _("Please be patient ...") . '</b>';
+       $msg .= '<br />';
+       $msg .= '<br />';
+       if ($secs < 60) {
+           $msg .= addslashes(sprintf(_("Can take up to %d seconds !"), $secs));
+       } else {
+           $min = intval($secs / 60);
+           if ($min == 1) {
+               $msg .= addslashes(_("Can take up to 1 minute !"));
+           } else {
+               $msg .= addslashes(sprintf(_("Can take up to %d minutes !"), $min));
+           }
+       }
+       
+       $msg .= '</td><td><img src="themes/graphics/redbox_spinner.gif">';
+       
+       $msg .= '</td></tr></table>';
+       echo '<script>';
+       echo 'RedBox.loading();';
+       echo "RedBox.showHtml('$msg');";
+       echo '</script>';
+       flush();
+    }
+
+    function RB_close() {
+       echo '<script>';
+       echo 'RedBox.close();';
+       echo '</script>';
+    }
+    
+}
diff --git a/babel/lib/api.php b/babel/lib/api.php
new file mode 100644 (file)
index 0000000..b5016e2
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+/**
+ * Babel external API interface.
+ *
+ * This file defines Babel's external API interface. Other applications can
+ * interact with Babel through this API.
+ *
+ * @package Babel
+ */
+
+$_services['perms'] = array(
+    'args' => array(),
+    // This is actually a hash of hashes
+    'type' => '{urn:horde}hash'
+);
+
+function _translation_perms()
+{
+
+    global $nls, $registry;
+    
+    static $perms = array();
+    if (!empty($perms)) {
+       return $perms;
+    }
+
+    $perms['tree']['translation']['language'] = array();
+    $perms['title']['translation:language'] = _("Languages");
+    $perms['type']['translation:language']  = 'none';
+    
+    foreach($nls['languages'] as $langcode => $langdesc) {
+       $perms['tree']['translation']['language'][$langcode] = false;
+       $perms['title']['translation:language:' . $langcode] = sprintf("%s (%s)", $langdesc, $langcode);
+       $perms['type']['translation:language:' . $langcode] = 'boolean';
+    }
+
+    $perms['tree']['translation']['module'] = array();
+    $perms['title']['translation:module'] = _("Modules");
+    $perms['type']['translation:module']  = 'none';
+    
+    foreach ($registry->applications as $app => $params) {
+       if ($params['status'] == 'heading' || $params['status'] == 'block') {
+           continue;
+       }
+       
+       if (isset($params['fileroot']) && !is_dir($params['fileroot'])) {
+           continue;
+       }
+       
+       if (preg_match('/_reports$/', $app) || preg_match('/_tools$/', $app)) {
+           continue;
+       }
+       
+       $perms['tree']['translation']['module'][$app] = false;
+       $perms['title']['translation:module:' . $app] = sprintf("%s (%s)", $params['name'], $app);
+       $perms['type']['translation:module:' . $app] = 'boolean';
+    }
+
+    $tabdesc['download']   = _("Download");
+    $tabdesc['upload']     = _("Upload");
+    $tabdesc['stats']      = _("Statistics");
+    $tabdesc['view']       = _("View/Edit");
+    $tabdesc['viewsource'] = _("View Source");
+    $tabdesc['extract']    = _("Extract");
+    $tabdesc['make']       = _("Make");
+    $tabdesc['commit']     = _("Commit");
+    $tabdesc['reset']      = _("Reset");
+    
+    foreach ($tabdesc as $cat => $desc) {
+       $perms['tree']['translation'][$cat] = array();
+       $perms['title']['translation:' . $cat] = $desc;
+    }
+    
+    return $perms;
+    
+}
diff --git a/babel/lib/base.php b/babel/lib/base.php
new file mode 100644 (file)
index 0000000..223530f
--- /dev/null
@@ -0,0 +1,126 @@
+<?php
+/**
+ * Copyright 2000-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author  Joel Vandal <joel@scopserv.com>
+ * @package Babel
+ */
+
+/* Check for a prior definition of HORDE_BASE (perhaps by an auto_prepend_file
+ * definition for site customization). */
+if (!defined('HORDE_BASE')) {
+    @define('HORDE_BASE', dirname(__FILE__) . '/../..');
+}
+
+if (!defined('BABEL_BASE')) {
+    @define('BABEL_BASE', dirname(__FILE__) . '/..');
+}
+
+/* Load the Horde Framework core, and set up inclusion paths. */
+require_once HORDE_BASE . '/lib/core.php';
+
+/* Notification system. */
+$notification = &Notification::singleton();
+$notification->attach('status');
+
+/* Registry. */
+$registry = &Registry::singleton();
+
+if (is_a(($pushed = $registry->pushApp('translation', !defined('AUTH_HANDLER'))), 'PEAR_Error')) {
+    if ($pushed->getCode() == 'permission_denied') {
+        Horde::authenticationFailureRedirect(); 
+    }
+    Horde::fatal($pushed, __FILE__, __LINE__, false);
+}
+
+$conf = &$GLOBALS['conf'];
+@define('BABEL_TEMPLATES', $registry->get('templates'));
+
+/* Horde base libraries */
+require_once 'Horde/Secret.php';
+
+/* Translation base library */
+require_once BABEL_BASE . '/lib/Translation.php';
+require_once BABEL_BASE . '/lib/Translate.php';
+require_once BABEL_BASE . '/lib/Translate_Help.php';
+require_once BABEL_BASE . '/lib/Display.php';
+
+/* Help */
+require_once 'Horde/Help.php';
+
+/* Menu */
+require_once 'Horde/Menu.php';
+
+/* Gettext (PO) */
+require_once BABEL_BASE . '/lib/Gettext/PO.php';
+
+/* Form and Variables */
+require_once 'Horde/Form.php';
+require_once 'Horde/Variables.php';
+require_once 'Horde/Form/Renderer.php';
+require_once 'Horde/Form/Action.php';
+
+/* Tabs and Pager UI */
+require_once 'Horde/UI/Tabs.php';
+require_once 'Horde/UI/Pager.php';
+
+/* Templates */
+require_once 'Horde/Template.php';
+$template = &new Horde_Template();
+
+/* Module selection */
+$app = Util::getFormData('module');
+  
+/* Language selection */
+if (($lang = Util::getFormData('display_language')) !== null) {
+    $_SESSION['translation']['language'] = $lang;
+} elseif (isset($_SESSION['translation']['language'])) {
+    $lang = $_SESSION['translation']['language'];
+} else {
+    
+    $tests =  $nls['languages'];
+    
+    // Unset English
+    unset($tests['en_US']);
+    
+    foreach($tests as $dir => $desc) {
+       if (!Translation::hasPermission("language:$dir")) {
+           continue;
+       } else {
+           $lang = $dir;
+           break;
+       }
+    }
+    $_SESSION['translation']['language'] = $lang;
+}
+                                                 
+/* Set up the template fields. */
+$template->set('menu', Translation::getMenu('string'));
+$template->set('notify', Util::bufferOutput(array($notification, 'notify'), array('listeners' => 'status')));
+$template->set('lang', Translation::displayLanguage());
+$fmenu = Translation::LanguageSelection();
+
+// Only display the Module Selection widget if an application has been set
+if ($app) {
+    $fmenu .= Translation::ModuleSelection();
+}
+$template->set('fmenu', $fmenu);
+
+if ($lang && !Translation::hasPermission("language:$lang")) {
+    Horde::fatal(sprintf(_("Access forbidden to '%s'."), $lang), __FILE__, __LINE__, true);
+}
+
+if ($app && !Translation::hasPermission("module:$app")) {
+    Horde::fatal(sprintf(_("Access forbidden to '%s'."), $app), __FILE__, __LINE__, true);
+}
+
+/* Custom sort function */
+function my_usort_function($a, $b)
+{
+    if ($a[1] > $b[1]) { return -1; }
+    if ($a[1] < $b[1]) { return 1; }
+    return 0;
+}
diff --git a/babel/make.php b/babel/make.php
new file mode 100644 (file)
index 0000000..cf54e7d
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+/**
+ * Copyright 2000-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author  Joel Vandal <joel@scopserv.com>
+ * @package Babel
+ */
+
+set_time_limit(0);
+
+@define('BABEL_BASE', dirname(__FILE__)) ;
+require_once BABEL_BASE . '/lib/base.php';
+
+if ($app) {
+    /* Render the page. */
+    Translation::RB_init();
+}
+
+require BABEL_TEMPLATES . '/common-header.inc';
+
+if ($app) {
+    Translation::RB_start(30);
+}
+
+echo $template->fetch(BABEL_TEMPLATES . '/layout.html');
+
+$vars = &Variables::getDefaultVariables();
+
+/* Create upload form */
+$form = &new Horde_Form($vars, _("Make Translation"), 'make');
+
+if (!$app) {
+    $form->setButtons(_("Make"));
+    $form->addVariable(_("Module"), 'module', 'enum', true, false, null, array(Translation::listApps(true), true));
+    $form->addVariable('', '', 'spacer', true);
+    
+    $renderer_params = array();
+    $renderer = &new Horde_Form_Renderer($renderer_params);
+    $renderer->setAttrColumnWidth('20%');
+    
+    $form->renderActive($renderer, $vars, Horde::selfURL(), 'post');
+} else {
+
+    if ($app != 'ALL') {
+       $module = $app;
+    }
+    
+    Translate::sanity_check();
+    
+    /* Searching applications */
+    Translate::check_binaries();
+    
+    Translate_Display::info(sprintf(_("Searching Horde applications in %s"), realpath(HORDE_BASE)));
+    $dirs = Translate::search_applications();
+        
+    $apps = Translate::strip_horde($dirs);
+    $apps[0] = 'horde';
+    Translate_Display::info(_("Found applications:"));
+    Translate_Display::info(wordwrap(implode(', ', $apps)), false);
+    Translate_Display::info();
+    
+    Translate_Display::header(_("Generate Compendium ..."));
+    Translate::compendium();
+    Translate_Display::info();
+    
+    Translate::cleanup(true);
+    Translate_Display::info();
+    Translate::make();
+    
+    Translation::RB_close();
+}
+
+require $registry->get('templates', 'horde') . '/common-footer.inc';
diff --git a/babel/po/fr_FR.po b/babel/po/fr_FR.po
new file mode 100644 (file)
index 0000000..01bdf9e
--- /dev/null
@@ -0,0 +1,493 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Translation ???\n"
+"Report-Msgid-Bugs-To: support@scopserv.com\n"
+"POT-Creation-Date: 2009-01-18 17:15-0500\n"
+"PO-Revision-Date: 2009-01-12 23:02-0500\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: i18n@lists.horde.org\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"
+
+#: lib/Translate.php:257 lib/Translate.php:276
+#, php-format
+msgid "%s is not writable."
+msgstr "%s n'est pas accessible en Ã©criture."
+
+#: extract.php:65 lib/Translate.php:94 lib/Translate.php:117
+#, php-format
+msgid "%s not found."
+msgstr "%s non trouvé."
+
+#: lib/Translate.php:428
+#, php-format
+msgid "%s not found. Run 'Extract' first."
+msgstr "%s non trouvé. Veuillez faire une 'Extraction'."
+
+#: view.php:301
+#, php-format
+msgid "%s to %s of %s"
+msgstr "%s à %s de %s"
+
+#: lib/Translate.php:302
+#, php-format
+msgid ""
+"<gettext> tag not closed in file %s.\n"
+"Opening tag found in line %d."
+msgstr ""
+"La balise <gettext> n'est pas fermé correctement dans le fichier %s. Balise "
+"d'ouverture trouvé Ã  la ligne %d."
+
+#: viewsource.php:30
+#, php-format
+msgid "Access denied to %s"
+msgstr "Accès refusé Ã  %s"
+
+#: lib/base.php:112 lib/base.php:116
+#, php-format
+msgid "Access forbidden to '%s'."
+msgstr "Accès refusé Ã  '%s'."
+
+#: view.php:223 view.php:225
+msgid "All"
+msgstr "Tout"
+
+#: lib/Translation.php:53 lib/Translation.php:135
+msgid "All Applications"
+msgstr "Toutes les applications"
+
+#: templates/index.php:50
+msgid "Build binary MO files from the specified PO files."
+msgstr "Construire les fichiers binaire MO Ã  partir des fichiers PO spécifié."
+
+#: lib/Translate.php:467
+#, php-format
+msgid "Building MO files for module %s..."
+msgstr "Génération des fichiers MO pour le module %s ..."
+
+#: lib/Translate.php:479
+#, php-format
+msgid "Building locale %s..."
+msgstr "Généré locale %s ..."
+
+#: lib/Translation.php:292
+#, php-format
+msgid "Can take up to %d minutes !"
+msgstr "Peut prendre jusqu'à %d minutes !"
+
+#: lib/Translation.php:286
+#, php-format
+msgid "Can take up to %d seconds !"
+msgstr "Peut prendre jusqu'à %d secondes !"
+
+#: lib/Translation.php:290
+msgid "Can take up to 1 minute !"
+msgstr "Peut prendre jusqu'à 1 minute !"
+
+#: view.php:442
+msgid "Cancel"
+msgstr "Annuler"
+
+#: lib/Translate.php:626
+#, php-format
+msgid "Cleaning up PO files for module %s..."
+msgstr "Nettoyage des fichiers PO pour le module %s ..."
+
+#: lib/Translate.php:638
+#, php-format
+msgid "Cleaning up locale %s..."
+msgstr "Netoyage de la locale %s ..."
+
+#: commit.php:42 templates/index.php:55 lib/api.php:66
+msgid "Commit"
+msgstr "Validez"
+
+#: commit.php:36
+msgid "Commit PO files ..."
+msgstr "Validez les fichier PO ..."
+
+#: templates/index.php:56
+msgid "Commit translations to the SVN server."
+msgstr "Validez (commit) les traductions sur le serveur SVN."
+
+#: extract.php:114 lib/Translate.php:374 lib/Translate.php:405
+#: lib/Translate.php:446 lib/Translate.php:536 lib/Translate.php:584
+#: lib/Translate.php:646
+msgid "Done!"
+msgstr "Terminé!"
+
+#: templates/index.php:25 lib/api.php:59
+msgid "Download"
+msgstr "Télécharger"
+
+#: download.php:14
+msgid "Download File"
+msgstr "Télécharger Fichier"
+
+#: templates/index.php:26
+msgid "Download all current PO files."
+msgstr "Télécharger tous les fichiers PO."
+
+#: edit.php:78
+msgid "Edit"
+msgstr "Modifier"
+
+#: view.php:164
+msgid "Edit Header"
+msgstr "Modifier Entête"
+
+#: view.php:229
+msgid "Edit Mode"
+msgstr "Mode Ã‰dition"
+
+#: edit.php:52 view.php:438
+msgid "Edit Translation"
+msgstr "Modifier Traduction"
+
+#: lib/Translate.php:347 lib/Translate.php:386
+msgid "Error: No locale specified."
+msgstr "Erreur: Il n'y a pas de locale de spécifié."
+
+#: extract.php:36 templates/index.php:43 lib/api.php:64
+msgid "Extract"
+msgstr "Extraire"
+
+#: extract.php:33
+msgid "Extract Translation"
+msgstr "Extraire Traduction"
+
+#: lib/Translate.php:242
+#, php-format
+msgid "Extracting from %s... "
+msgstr "Extraction Ã  partir de %s ..."
+
+#: lib/Translate.php:407 lib/Translate.php:427 lib/Translate.php:448
+#: lib/Translate.php:564 lib/Translate.php:586 lib/Translate.php:649
+msgid "Failed!"
+msgstr "Échoué!"
+
+#: config/hooks.php:24
+msgid "File doesn't exist ... "
+msgstr "Fichier non existant ..."
+
+#: view.php:220
+msgid "Filter: "
+msgstr "Filtres: "
+
+#: commit.php:32 make.php:61 reset.php:34 extract.php:92
+msgid "Found applications:"
+msgstr "Applications Trouvée :"
+
+#: lib/Translation.php:24
+#, php-format
+msgid "Function doesn't exist: %s"
+msgstr "Fonction non-existante: %s"
+
+#: view.php:198 view.php:244 view.php:246 stats.php:81
+msgid "Fuzzy"
+msgstr "Floue"
+
+#: make.php:65 extract.php:105
+msgid "Generate Compendium ..."
+msgstr "Génération du Compendium ..."
+
+#: templates/index.php:44
+msgid "Generate and merge PO files."
+msgstr "Générer et fusionner les fichier PO."
+
+#: templates/index.php:38
+msgid "Get statistics about translations."
+msgstr "Obtenir les statistiques concernant les traductions."
+
+#: extract.php:50 lib/Translate.php:80
+msgid "Gettext extension not found!"
+msgstr "Extension Gettext non trouvé!"
+
+#: lib/Translation.php:27
+msgid "Hook file doesn't exist"
+msgstr "Fichier Hook non existant"
+
+#: commit.php:19 reset.php:24 stats.php:42 extract.php:46
+msgid "Horde translation generator"
+msgstr "Générateur de traduction Horde"
+
+#: lib/Translate.php:425
+#, php-format
+msgid "Initializing module %s..."
+msgstr "Initialisation du module %s ..."
+
+#: upload.php:37
+msgid "Invalid Translations file. Please submit a valid PO file!"
+msgstr ""
+"Fichier de traductions invalide. Veuillez soumettre un fichier PO valide !"
+
+#: view.php:194 stats.php:77
+msgid "Language"
+msgstr "Langue"
+
+#: lib/Translation.php:38
+#, php-format
+msgid "Language: %s (%s)"
+msgstr "Langue: %s (%s)"
+
+#: lib/api.php:28
+msgid "Languages"
+msgstr "Langues"
+
+#: extract.php:54 lib/Translate.php:83
+msgid "Loading libraries..."
+msgstr "Chargement des librairies ..."
+
+#: view.php:195 stats.php:78
+msgid "Locale"
+msgstr "Locale"
+
+#: view.php:430
+#, php-format
+msgid "Locked by %s"
+msgstr "Verouillé par %s"
+
+#: make.php:36 templates/index.php:49 lib/api.php:65
+msgid "Make"
+msgstr "Générer"
+
+#: make.php:33
+msgid "Make Translation"
+msgstr "Générer la Traduction"
+
+#: lib/Translate.php:101
+msgid "Make sure that you have PEAR installed and in your include path."
+msgstr ""
+"Veuillez vérifier que vous avez installé PEAR et qu'il sont include dans le "
+"chemin d'accès (include path)."
+
+#: lib/Translate.php:390
+#, php-format
+msgid "Merging all %s.po files to the compendium... "
+msgstr "Fusionner tous les fichiers %s.po vers le compendium..."
+
+#: lib/Translate.php:371
+#, php-format
+msgid "Merging locale %s..."
+msgstr "Fusion de la locale %s ..."
+
+#: lib/Translate.php:574
+#, php-format
+msgid "Merging the PO file for %s to the compendium..."
+msgstr "Fusionner tous les fichiers PO pour %s vers le compendium..."
+
+#: lib/Translate.php:358
+#, php-format
+msgid "Merging translation for module %s..."
+msgstr "Fusion de la traduction pour le module %s ..."
+
+#: view.php:175
+msgid "Meta Informations"
+msgstr "Informations Meta"
+
+#: viewsource.php:25
+msgid "Missing filename!"
+msgstr "Nom de fichier manquant!"
+
+#: edit.php:79 make.php:37 view.php:153 upload.php:20 stats.php:33
+#: extract.php:37
+msgid "Module"
+msgstr "Module"
+
+#: lib/Translation.php:40
+#, php-format
+msgid "Module: %s"
+msgstr "Module: %s"
+
+#: lib/api.php:38
+msgid "Modules"
+msgstr "Modules"
+
+#: lib/Translate.php:130
+msgid "Not all strings will be extracted."
+msgstr "Les chaînes de caractères n'ont pas toutes Ã©té extraite."
+
+#: lib/Translate.php:333
+msgid "Not changed!"
+msgstr "Non modifié!"
+
+#: view.php:200 stats.php:83
+msgid "Obsolete"
+msgstr "Obsolète"
+
+#: lib/Translation.php:282
+msgid "Please be patient ..."
+msgstr "Veuillez patienter..."
+
+#: upload.php:31
+msgid "Please select module of translations PO file!"
+msgstr "Veuillez sélectionner le module pour le fichier PO de traduction."
+
+#: templates/index.php:61 lib/api.php:67
+msgid "Reset"
+msgstr "Réinitialiser"
+
+#: reset.php:43
+msgid "Reset PO file on "
+msgstr "Ré-initialisation du fichier PO pour "
+
+#: reset.php:38
+msgid "Reset PO files ..."
+msgstr "Ré-initialisation des fichiers PO ..."
+
+#: lib/Translate.php:595
+msgid "Results (including Horde):"
+msgstr "Résultats (incluant Horde):"
+
+#: lib/Translate.php:593
+msgid "Results:"
+msgstr "Résultats:"
+
+#: edit.php:55 edit.php:83 view.php:440
+msgid "Save"
+msgstr "Enregistrer"
+
+#: view.php:267
+msgid "Search"
+msgstr "Recherche"
+
+#: lib/Translate.php:110
+msgid "Searching gettext binaries..."
+msgstr "Recherche des exécutables pour gettext ..."
+
+#: lib/Translate.php:364 lib/Translate.php:472 lib/Translate.php:631
+msgid "Skipped..."
+msgstr "Ignoré..."
+
+#: view.php:188
+msgid "Statistic"
+msgstr "Statistique"
+
+#: templates/index.php:37 lib/api.php:61
+msgid "Statistics"
+msgstr "Statistiques"
+
+#: view.php:196 stats.php:79
+msgid "Status"
+msgstr "Statut"
+
+#: templates/index.php:62
+msgid ""
+"The reset procedure will delete all PO files from the server for all modules "
+"and restore from SVN server."
+msgstr ""
+"La procédure de ré-initialisation effacera tous les fichiers PO du serveur "
+"et les restaurera Ã  partir du serveur SVN."
+
+#: view.php:197 view.php:233 view.php:235 stats.php:80
+msgid "Translated"
+msgstr "Traduit"
+
+#: view.php:302
+msgid "Translations"
+msgstr "Traductions"
+
+#: upload.php:21
+msgid "Translations File (.PO)"
+msgstr "Fichier de Traduction (.PO)"
+
+#: view.php:199 view.php:254 view.php:256 stats.php:82
+msgid "Untranslated"
+msgstr "Non Traduit"
+
+#: lib/Translate.php:330
+msgid "Updated!"
+msgstr "Mis Ã  jour!"
+
+#: upload.php:19 upload.php:25 templates/index.php:31 lib/api.php:60
+msgid "Upload"
+msgstr "Envoi"
+
+#: templates/index.php:32
+msgid "Upload new PO files."
+msgstr "Envoyer nouveaux fichiers PO."
+
+#: upload.php:18
+msgid "Upload new Translation"
+msgstr "Envoi une nouvelle Traduction"
+
+#: upload.php:45
+#, php-format
+msgid "Upload successful for %s (%s)"
+msgstr "Transfert de fichier réussi pour %s (%s)"
+
+#: view.php:152 stats.php:32 templates/index.php:19
+msgid "View"
+msgstr "Voir"
+
+#: lib/api.php:63
+msgid "View Source"
+msgstr "Visionner Source"
+
+#: stats.php:29
+msgid "View Statistics"
+msgstr "Voir Statistiques"
+
+#: view.php:149
+msgid "View Translation"
+msgstr "Voir Traduction"
+
+#: templates/index.php:20
+msgid "View all translations."
+msgstr "Visionner toutes les traductions."
+
+#: viewsource.php:39
+#, php-format
+msgid "View source: %s"
+msgstr "Voir source: %s"
+
+#: lib/api.php:62
+msgid "View/Edit"
+msgstr "Visionner/Modifier"
+
+#: lib/Translate.php:484
+#, php-format
+msgid "Warning: Could not create locale directory for locale %s:"
+msgstr "Avertissement: Impossible de créer le dossier pour le locale %s :"
+
+#: lib/Translate.php:129
+msgid ""
+"Warning: Your gettext version is too old and does not support PHP natively."
+msgstr ""
+"Avertissement: Votre version de gettext est trop ancienne et ne supporte pas "
+"PHP nativement."
+
+#: lib/Translate.php:506
+msgid "Warning: an error has occured:"
+msgstr "Avertissement: Une erreur s'est produite:"
+
+#: lib/Translate.php:520
+#, php-format
+msgid "Warning: the Horde PO file for the locale %s does not exist:"
+msgstr "Avertissement: le fichier PO de Horde pour le locale %s n'existe pas:"
+
+#: lib/Translation.php:214
+msgid "_Extract"
+msgstr "_Extraire"
+
+#: lib/Translation.php:197
+msgid "_General"
+msgstr "_Général"
+
+#: lib/Translation.php:220
+msgid "_Make"
+msgstr "Gé_nérer"
+
+#: lib/Translation.php:208
+msgid "_Stats"
+msgstr "_Stats"
+
+#: lib/Translation.php:226
+msgid "_Upload"
+msgstr "En_voi"
+
+#: lib/Translation.php:202
+msgid "_View"
+msgstr "_Visionner"
diff --git a/babel/po/translation.pot b/babel/po/translation.pot
new file mode 100644 (file)
index 0000000..1421ca3
--- /dev/null
@@ -0,0 +1,489 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR Horde Project
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: support@scopserv.com\n"
+"POT-Creation-Date: 2009-01-18 17:15-0500\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: lib/Translate.php:257 lib/Translate.php:276
+#, php-format
+msgid "%s is not writable."
+msgstr ""
+
+#: extract.php:65 lib/Translate.php:94 lib/Translate.php:117
+#, php-format
+msgid "%s not found."
+msgstr ""
+
+#: lib/Translate.php:428
+#, php-format
+msgid "%s not found. Run 'Extract' first."
+msgstr ""
+
+#: view.php:301
+#, php-format
+msgid "%s to %s of %s"
+msgstr ""
+
+#: lib/Translate.php:302
+#, php-format
+msgid ""
+"<gettext> tag not closed in file %s.\n"
+"Opening tag found in line %d."
+msgstr ""
+
+#: viewsource.php:30
+#, php-format
+msgid "Access denied to %s"
+msgstr ""
+
+#: lib/base.php:112 lib/base.php:116
+#, php-format
+msgid "Access forbidden to '%s'."
+msgstr ""
+
+#: view.php:223 view.php:225
+msgid "All"
+msgstr ""
+
+#: lib/Translation.php:53 lib/Translation.php:135
+msgid "All Applications"
+msgstr ""
+
+#: templates/index.php:50
+msgid "Build binary MO files from the specified PO files."
+msgstr ""
+
+#: lib/Translate.php:467
+#, php-format
+msgid "Building MO files for module %s..."
+msgstr ""
+
+#: lib/Translate.php:479
+#, php-format
+msgid "Building locale %s..."
+msgstr ""
+
+#: lib/Translation.php:292
+#, php-format
+msgid "Can take up to %d minutes !"
+msgstr ""
+
+#: lib/Translation.php:286
+#, php-format
+msgid "Can take up to %d seconds !"
+msgstr ""
+
+#: lib/Translation.php:290
+msgid "Can take up to 1 minute !"
+msgstr ""
+
+#: view.php:442
+msgid "Cancel"
+msgstr ""
+
+#: lib/Translate.php:626
+#, php-format
+msgid "Cleaning up PO files for module %s..."
+msgstr ""
+
+#: lib/Translate.php:638
+#, php-format
+msgid "Cleaning up locale %s..."
+msgstr ""
+
+#: commit.php:42 templates/index.php:55 lib/api.php:66
+msgid "Commit"
+msgstr ""
+
+#: commit.php:36
+msgid "Commit PO files ..."
+msgstr ""
+
+#: templates/index.php:56
+msgid "Commit translations to the SVN server."
+msgstr ""
+
+#: extract.php:114 lib/Translate.php:374 lib/Translate.php:405
+#: lib/Translate.php:446 lib/Translate.php:536 lib/Translate.php:584
+#: lib/Translate.php:646
+msgid "Done!"
+msgstr ""
+
+#: templates/index.php:25 lib/api.php:59
+msgid "Download"
+msgstr ""
+
+#: download.php:14
+msgid "Download File"
+msgstr ""
+
+#: templates/index.php:26
+msgid "Download all current PO files."
+msgstr ""
+
+#: edit.php:78
+msgid "Edit"
+msgstr ""
+
+#: view.php:164
+msgid "Edit Header"
+msgstr ""
+
+#: view.php:229
+msgid "Edit Mode"
+msgstr ""
+
+#: edit.php:52 view.php:438
+msgid "Edit Translation"
+msgstr ""
+
+#: lib/Translate.php:347 lib/Translate.php:386
+msgid "Error: No locale specified."
+msgstr ""
+
+#: extract.php:36 templates/index.php:43 lib/api.php:64
+msgid "Extract"
+msgstr ""
+
+#: extract.php:33
+msgid "Extract Translation"
+msgstr ""
+
+#: lib/Translate.php:242
+#, php-format
+msgid "Extracting from %s... "
+msgstr ""
+
+#: lib/Translate.php:407 lib/Translate.php:427 lib/Translate.php:448
+#: lib/Translate.php:564 lib/Translate.php:586 lib/Translate.php:649
+msgid "Failed!"
+msgstr ""
+
+#: config/hooks.php:24
+msgid "File doesn't exist ... "
+msgstr ""
+
+#: view.php:220
+msgid "Filter: "
+msgstr ""
+
+#: commit.php:32 make.php:61 reset.php:34 extract.php:92
+msgid "Found applications:"
+msgstr ""
+
+#: lib/Translation.php:24
+#, php-format
+msgid "Function doesn't exist: %s"
+msgstr ""
+
+#: view.php:198 view.php:244 view.php:246 stats.php:81
+msgid "Fuzzy"
+msgstr ""
+
+#: make.php:65 extract.php:105
+msgid "Generate Compendium ..."
+msgstr ""
+
+#: templates/index.php:44
+msgid "Generate and merge PO files."
+msgstr ""
+
+#: templates/index.php:38
+msgid "Get statistics about translations."
+msgstr ""
+
+#: extract.php:50 lib/Translate.php:80
+msgid "Gettext extension not found!"
+msgstr ""
+
+#: lib/Translation.php:27
+msgid "Hook file doesn't exist"
+msgstr ""
+
+#: commit.php:19 reset.php:24 stats.php:42 extract.php:46
+msgid "Horde translation generator"
+msgstr ""
+
+#: lib/Translate.php:425
+#, php-format
+msgid "Initializing module %s..."
+msgstr ""
+
+#: upload.php:37
+msgid "Invalid Translations file. Please submit a valid PO file!"
+msgstr ""
+
+#: view.php:194 stats.php:77
+msgid "Language"
+msgstr ""
+
+#: lib/Translation.php:38
+#, php-format
+msgid "Language: %s (%s)"
+msgstr ""
+
+#: lib/api.php:28
+msgid "Languages"
+msgstr ""
+
+#: extract.php:54 lib/Translate.php:83
+msgid "Loading libraries..."
+msgstr ""
+
+#: view.php:195 stats.php:78
+msgid "Locale"
+msgstr ""
+
+#: view.php:430
+#, php-format
+msgid "Locked by %s"
+msgstr ""
+
+#: make.php:36 templates/index.php:49 lib/api.php:65
+msgid "Make"
+msgstr ""
+
+#: make.php:33
+msgid "Make Translation"
+msgstr ""
+
+#: lib/Translate.php:101
+msgid "Make sure that you have PEAR installed and in your include path."
+msgstr ""
+
+#: lib/Translate.php:390
+#, php-format
+msgid "Merging all %s.po files to the compendium... "
+msgstr ""
+
+#: lib/Translate.php:371
+#, php-format
+msgid "Merging locale %s..."
+msgstr ""
+
+#: lib/Translate.php:574
+#, php-format
+msgid "Merging the PO file for %s to the compendium..."
+msgstr ""
+
+#: lib/Translate.php:358
+#, php-format
+msgid "Merging translation for module %s..."
+msgstr ""
+
+#: view.php:175
+msgid "Meta Informations"
+msgstr ""
+
+#: viewsource.php:25
+msgid "Missing filename!"
+msgstr ""
+
+#: edit.php:79 make.php:37 view.php:153 upload.php:20 stats.php:33
+#: extract.php:37
+msgid "Module"
+msgstr ""
+
+#: lib/Translation.php:40
+#, php-format
+msgid "Module: %s"
+msgstr ""
+
+#: lib/api.php:38
+msgid "Modules"
+msgstr ""
+
+#: lib/Translate.php:130
+msgid "Not all strings will be extracted."
+msgstr ""
+
+#: lib/Translate.php:333
+msgid "Not changed!"
+msgstr ""
+
+#: view.php:200 stats.php:83
+msgid "Obsolete"
+msgstr ""
+
+#: lib/Translation.php:282
+msgid "Please be patient ..."
+msgstr ""
+
+#: upload.php:31
+msgid "Please select module of translations PO file!"
+msgstr ""
+
+#: templates/index.php:61 lib/api.php:67
+msgid "Reset"
+msgstr ""
+
+#: reset.php:43
+msgid "Reset PO file on "
+msgstr ""
+
+#: reset.php:38
+msgid "Reset PO files ..."
+msgstr ""
+
+#: lib/Translate.php:595
+msgid "Results (including Horde):"
+msgstr ""
+
+#: lib/Translate.php:593
+msgid "Results:"
+msgstr ""
+
+#: edit.php:55 edit.php:83 view.php:440
+msgid "Save"
+msgstr ""
+
+#: view.php:267
+msgid "Search"
+msgstr ""
+
+#: lib/Translate.php:110
+msgid "Searching gettext binaries..."
+msgstr ""
+
+#: lib/Translate.php:364 lib/Translate.php:472 lib/Translate.php:631
+msgid "Skipped..."
+msgstr ""
+
+#: view.php:188
+msgid "Statistic"
+msgstr ""
+
+#: templates/index.php:37 lib/api.php:61
+msgid "Statistics"
+msgstr ""
+
+#: view.php:196 stats.php:79
+msgid "Status"
+msgstr ""
+
+#: templates/index.php:62
+msgid ""
+"The reset procedure will delete all PO files from the server for all modules "
+"and restore from SVN server."
+msgstr ""
+
+#: view.php:197 view.php:233 view.php:235 stats.php:80
+msgid "Translated"
+msgstr ""
+
+#: view.php:302
+msgid "Translations"
+msgstr ""
+
+#: upload.php:21
+msgid "Translations File (.PO)"
+msgstr ""
+
+#: view.php:199 view.php:254 view.php:256 stats.php:82
+msgid "Untranslated"
+msgstr ""
+
+#: lib/Translate.php:330
+msgid "Updated!"
+msgstr ""
+
+#: upload.php:19 upload.php:25 templates/index.php:31 lib/api.php:60
+msgid "Upload"
+msgstr ""
+
+#: templates/index.php:32
+msgid "Upload new PO files."
+msgstr ""
+
+#: upload.php:18
+msgid "Upload new Translation"
+msgstr ""
+
+#: upload.php:45
+#, php-format
+msgid "Upload successful for %s (%s)"
+msgstr ""
+
+#: view.php:152 stats.php:32 templates/index.php:19
+msgid "View"
+msgstr ""
+
+#: lib/api.php:63
+msgid "View Source"
+msgstr ""
+
+#: stats.php:29
+msgid "View Statistics"
+msgstr ""
+
+#: view.php:149
+msgid "View Translation"
+msgstr ""
+
+#: templates/index.php:20
+msgid "View all translations."
+msgstr ""
+
+#: viewsource.php:39
+#, php-format
+msgid "View source: %s"
+msgstr ""
+
+#: lib/api.php:62
+msgid "View/Edit"
+msgstr ""
+
+#: lib/Translate.php:484
+#, php-format
+msgid "Warning: Could not create locale directory for locale %s:"
+msgstr ""
+
+#: lib/Translate.php:129
+msgid ""
+"Warning: Your gettext version is too old and does not support PHP natively."
+msgstr ""
+
+#: lib/Translate.php:506
+msgid "Warning: an error has occured:"
+msgstr ""
+
+#: lib/Translate.php:520
+#, php-format
+msgid "Warning: the Horde PO file for the locale %s does not exist:"
+msgstr ""
+
+#: lib/Translation.php:214
+msgid "_Extract"
+msgstr ""
+
+#: lib/Translation.php:197
+msgid "_General"
+msgstr ""
+
+#: lib/Translation.php:220
+msgid "_Make"
+msgstr ""
+
+#: lib/Translation.php:208
+msgid "_Stats"
+msgstr ""
+
+#: lib/Translation.php:226
+msgid "_Upload"
+msgstr ""
+
+#: lib/Translation.php:202
+msgid "_View"
+msgstr ""
diff --git a/babel/reset.php b/babel/reset.php
new file mode 100644 (file)
index 0000000..01ae5fe
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+/**
+ * Copyright 2000-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author  Joel Vandal <joel@scopserv.com>
+ * @package Babel
+ */
+
+@define('BABEL_BASE', dirname(__FILE__)) ;
+require_once BABEL_BASE . '/lib/base.php';
+
+Translation::RB_init();
+
+/* Render the page. */
+require BABEL_TEMPLATES . '/common-header.inc';
+
+Translation::RB_start(15);
+
+echo $template->fetch(BABEL_TEMPLATES . '/layout.html');
+
+Translate_Display::header(_("Horde translation generator"));
+
+Translate_Display::info(sprintf('Searching Horde applications in %s', realpath(HORDE_BASE)));
+
+Translate::check_binaries();
+
+$dirs = Translate::search_applications();
+
+$apps = Translate::strip_horde($dirs);
+$apps[0] = 'horde';
+Translate_Display::info(_("Found applications:"));
+Translate_Display::info(wordwrap(implode(', ', $apps)), false);
+Translate_Display::info();
+
+Translate_Display::header(_("Reset PO files ..."));
+foreach($dirs as $d => $dir) {
+    $dir = realpath($dir);
+    $po  = $dir . '/po/' . $lang . '.po';
+    
+    Translate_Display::info(_("Reset PO file on ") . $po);
+    Translation::callHook('reset', $po);
+
+}
+
+Translate_Display::info();
+Translate::cleanup(true);
+
+Translation::RB_close();
+
+require $registry->get('templates', 'horde') . '/common-footer.inc';
diff --git a/babel/scripts/translate.php b/babel/scripts/translate.php
new file mode 100755 (executable)
index 0000000..7f65fde
--- /dev/null
@@ -0,0 +1,1527 @@
+#!/usr/bin/php -q
+<?php
+/**
+ * Translation helper application for the Horde framework.
+ *
+ * For usage information call:
+ * ./translation.php help
+ *
+ * $Horde: horde/po/translation.php,v 1.105 2005/12/30 01:05:51 selsky Exp $
+ */
+
+function footer()
+{
+    global $c, $curdir;
+
+    $c->writeln();
+    $c->writeln('Please report any bugs to i18n@lists.horde.org.');
+
+    chdir($curdir);
+    exit;
+}
+
+function usage()
+{
+    global $options, $c;
+
+    if (count($options[1]) &&
+        ($options[1][0] == 'help' && !empty($options[1][1]) ||
+        !empty($options[1][0]) && in_array($options[1][0], array('commit', 'compendium', 'extract', 'init', 'make', 'merge')))) {
+        if ($options[1][0] == 'help') {
+            $cmd = $options[1][1];
+        } else {
+            $cmd = $options[1][0];
+        }
+        $c->writeln('Usage:' . ' translation.php [options] ' . $cmd . ' [command-options]');
+        if (!empty($cmd)) {
+            $c->writeln();
+            $c->writeln('Command options:');
+        }
+        switch ($cmd) {
+            case 'cleanup':
+                $c->writeln('  -l, --locale=ll_CC     Use only this locale.');
+                $c->writeln('  -m, --module=MODULE    Cleanup PO files only for this (Horde) module.');
+                break;
+            case 'commit':
+            case 'commit-help':
+                $c->writeln('  -l, --locale=ll_CC     Use this locale.');
+                $c->writeln('  -m, --module=MODULE    Commit translations only for this (Horde) module.');
+                $c->writeln('  -M, --message=MESSAGE  Use this commit message instead of the default ones.');
+                $c->writeln('  -n, --new              This is a new translation, commit also CREDITS,');
+                $c->writeln('                         CHANGES and nls.php.dist.');
+                break;
+            case 'compendium':
+                $c->writeln('  -a, --add=FILE        Add this PO file to the compendium. Useful to');
+                $c->writeln('                        include a compendium from a different branch to');
+                $c->writeln('                        the generated compendium.');
+                $c->writeln('  -d, --directory=DIR   Create compendium in this directory.');
+                $c->writeln('  -l, --locale=ll_CC    Use this locale.');
+                break;
+            case 'extract':
+                $c->writeln('  -m, --module=MODULE  Generate POT file only for this (Horde) module.');
+                break;
+            case 'init':
+                $c->writeln('  -l, --locale=ll_CC     Use this locale.');
+                $c->writeln('  -m, --module=MODULE    Create a PO file only for this (Horde) module.');
+                $c->writeln('  -c, --compendium=FILE  Use this compendium file instead of the default');
+                $c->writeln('                         one (compendium.po in the horde/po directory).');
+                $c->writeln('  -n, --no-compendium    Don\'t use a compendium.');
+                break;
+            case 'make':
+                $c->writeln('  -l, --locale=ll_CC     Use only this locale.');
+                $c->writeln('  -m, --module=MODULE    Build MO files only for this (Horde) module.');
+                $c->writeln('  -c, --compendium=FILE  Merge new translations to this compendium file');
+                $c->writeln('                         instead of the default one (compendium.po in the');
+                $c->writeln('                         horde/po directory.');
+                $c->writeln('  -n, --no-compendium    Don\'t merge new translations to the compendium.');
+                $c->writeln('  -s, --statistics       Save translation statistics in a local file.');
+                break;
+            case 'make-help':
+            case 'update-help':
+                $c->writeln('  -l, --locale=ll_CC     Use only this locale.');
+                $c->writeln('  -m, --module=MODULE    Update help files only for this (Horde) module.');
+                break;
+            case 'merge':
+                $c->writeln('  -l, --locale=ll_CC     Use this locale.');
+                $c->writeln('  -m, --module=MODULE    Merge PO files only for this (Horde) module.');
+                $c->writeln('  -c, --compendium=FILE  Use this compendium file instead of the default');
+                $c->writeln('                         one (compendium.po in the horde/po directory).');
+                $c->writeln('  -n, --no-compendium    Don\'t use a compendium.');
+                break;
+            case 'update':
+                $c->writeln('  -l, --locale=ll_CC     Use this locale.');
+                $c->writeln('  -m, --module=MODULE    Update only this (Horde) module.');
+                $c->writeln('  -c, --compendium=FILE  Use this compendium file instead of the default');
+                $c->writeln('                         one (compendium.po in the horde/po directory).');
+                $c->writeln('  -n, --no-compendium    Don\'t use a compendium.');
+                break;
+        }
+    } else {
+        $c->writeln('Usage:' . ' translation.php [options] command [command-options]');
+        $c->writeln(str_repeat(' ', String::length('Usage:')) . ' translation.php [help|-h|--help] [command]');
+        $c->writeln();
+        $c->writeln('Helper application to create and maintain translations for the Horde');
+        $c->writeln('framework and its applications.');
+        $c->writeln('For an introduction read the file README in this directory.');
+        $c->writeln();
+        $c->writeln('Commands:');
+        $c->writeln('  help        Show this help message.');
+        $c->writeln('  compendium  Rebuild the compendium file. Warning: This overwrites the');
+        $c->writeln('              current compendium.');
+        $c->writeln('  extract     Generate PO template (.pot) files.');
+        $c->writeln('  init        Create one or more PO files for a new locale. Warning: This');
+        $c->writeln('              overwrites the existing PO files of this locale.');
+        $c->writeln('  merge       Merge the current PO file with the current PO template file.');
+        $c->writeln('  update      Run extract and merge sequent.');
+        $c->writeln('  update-help Extract all new and changed entries from the English XML help');
+        $c->writeln('              file and merge them with the existing ones.');
+        $c->writeln('  cleanup     Cleans the PO files up from untranslated and obsolete entries.');
+        $c->writeln('  make        Build binary MO files from the specified PO files.');
+        $c->writeln('  make-help   Mark all entries in the XML help file being up-to-date and');
+        $c->writeln('              prepare the file for the next execution of update-help. You');
+        $c->writeln('              should only run make-help AFTER update-help and revising the');
+        $c->writeln('              help file.');
+        $c->writeln('  commit      Commit translations to the CVS server.');
+        $c->writeln('  commit-help Commit help files to the CVS server.');
+    }
+    $c->writeln();
+    $c->writeln('Options:');
+    $c->writeln('  -b, --base=/PATH  Full path to the (Horde) base directory that should be');
+    $c->writeln('                    used.');
+    $c->writeln('  -d, --debug       Show error messages from the executed binaries.');
+    $c->writeln('  -h, --help        Show this help message.');
+    $c->writeln('  -t, --test        Show the executed commands but don\'t run anything.');
+}
+
+function check_binaries()
+{
+    global $gettext_version, $c;
+
+    $c->writeln('Searching gettext binaries...');
+    require_once 'System.php';
+    foreach (array('gettext', 'msgattrib', 'msgcat', 'msgcomm', 'msgfmt', 'msginit', 'msgmerge', 'xgettext') as $binary) {
+        echo $binary . '... ';
+        $GLOBALS[$binary] = System::which($binary);
+        if ($GLOBALS[$binary]) {
+            $c->writeln($c->green('found: ') . $GLOBALS[$binary]);
+        } else {
+            $c->writeln($c->red('not found'));
+            footer();
+        }
+    }
+    $c->writeln();
+
+    $out = '';
+    exec($GLOBALS['gettext'] . ' --version', $out, $ret);
+    $split = explode(' ', $out[0]);
+    echo 'gettext version: ' . $split[count($split) - 1];
+    $gettext_version = explode('.', $split[count($split) - 1]);
+    if ($gettext_version[0] == 0 && $gettext_version[1] < 12) {
+        $GLOBALS['php_support'] = false;
+        $c->writeln();
+        $c->writeln($c->red('Warning: ') . 'Your gettext version is too old and does not support PHP natively.');
+        $c->writeln('Not all strings will be extracted.');
+    } else {
+        $GLOBALS['php_support'] = true;
+        $c->writeln($c->green(' ' . 'OK'));
+    }
+    $c->writeln();
+}
+
+function search_file($file, $dir = '.', $local = false)
+{
+    static $ff;
+    if (!isset($ff)) {
+        $ff = &new File_Find();
+    }
+
+    if (substr($file, 0, 1) != DS) {
+        $file = "/$file/";
+    }
+
+    if ($local) {
+        $files = $ff->glob($file, $dir, 'perl');
+        $files = array_map(create_function('$file', 'return "' . $dir . DS . '" . $file;'), $files);
+        return $files;
+    } else {
+        return $ff->search($file, $dir, 'perl');
+    }
+}
+
+function search_ext($ext, $dir = '.', $local = false)
+{
+    return search_file(".+\\.$ext\$", $dir, $local);
+}
+
+function get_po_files($dir)
+{
+    $langs = search_ext('po', $dir);
+    if (($key = array_search($dir . DS . 'messages.po', $langs)) !== false) {
+        unset($langs[$key]);
+    }
+    if (($key = array_search($dir . DS . 'compendium.po', $langs)) !== false) {
+        unset($langs[$key]);
+    }
+    return $langs;
+}
+
+function get_languages($dir)
+{
+    global $curdir;
+
+    chdir($dir);
+    $langs = get_po_files('po');
+    $langs = array_map(create_function('$lang', 'return str_replace("po" . DS, "", str_replace(".po", "", $lang));'), $langs);
+    chdir($curdir);
+    return $langs;
+}
+
+function search_applications()
+{
+    $dirs = array();
+    $horde = false;
+    if (@is_dir(BASE . DS . 'po')) {
+        $dirs[] = BASE;
+        $horde = true;
+    }
+    $dh = @opendir(BASE);
+    if ($dh) {
+        while ($entry = @readdir($dh)) {
+            $dir = BASE . DS . $entry;
+            if (is_dir($dir) &&
+                substr($entry, 0, 1) != '.' &&
+                fileinode(HORDE_BASE) != fileinode($dir)) {
+                $sub = opendir($dir);
+                if ($sub) {
+                    while ($subentry = readdir($sub)) {
+                        if ($subentry == 'po' && is_dir($dir . DS . $subentry)) {
+                            $dirs[] = $dir;
+                            if ($entry == 'horde') {
+                                $horde = true;
+                            }
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+        if (!$horde) {
+            array_unshift($dirs, HORDE_BASE);
+        }
+    }
+
+    return $dirs;
+}
+
+function strip_horde($file)
+{
+    if (is_array($file)) {
+        return array_map(create_function('$file', 'return strip_horde($file);'), $file);
+    } else {
+        return str_replace(BASE . DS, '', $file);
+    }
+}
+
+function xtract()
+{
+    global $cmd_options, $apps, $dirs, $debug, $test, $c, $gettext_version, $silence, $curdir;
+
+    foreach ($cmd_options[0] as $option) {
+        switch ($option[0]) {
+            case 'h':
+                usage();
+                footer();
+            case 'm':
+            case '--module':
+                $module = $option[1];
+                break;
+        }
+    }
+    require_once 'Horde/Array.php';
+    if ($GLOBALS['php_support']) {
+        $language = 'PHP';
+    } else {
+        $language = 'C++';
+    }
+    for ($i = 0; $i < count($dirs); $i++) {
+        if (!empty($module) && $module != $apps[$i]) {
+            continue;
+        }
+        echo sprintf('Extracting from %s... ', $apps[$i]);
+        chdir($dirs[$i]);
+        if ($apps[$i] == 'horde') {
+            $files = search_ext('php', '.', true);
+            foreach (array('admin', 'framework', 'lib', 'services', 'templates', 'util', 'config' . DS . 'themes') as $search_dir) {
+                $files = array_merge($files, search_ext('(php|inc|js)', $search_dir));
+            }
+            $files = array_merge($files, search_ext('(php|dist)', 'config'));
+            $sh = $GLOBALS['xgettext'] . ' --language=' . $language .
+                  ' --from-code=iso-8859-1 --keyword=_ --sort-output --copyright-holder="Horde Project"';
+            if ($gettext_version[0] > 0 || $gettext_version[1] > 11) {
+                $sh .= ' --msgid-bugs-address="dev@lists.horde.org"';
+            }
+            $file = $dirs[$i] . DS . 'po' . DS . $apps[$i] . '.pot';
+            if (file_exists($file) && !is_writable($file)) {
+                $c->writeln($c->red(sprintf('%s is not writable.', $file)));
+                footer();
+            }
+            $tmp_file = $file . '.tmp.pot';
+            $sh .= ' -o ' . $tmp_file . ' ' . implode(' ', $files);
+            if (@file_exists($dirs[$i] . '/po/translation.php')) {
+                $sh .= ' po/translation.php';
+            }
+            if (!$debug) {
+                $sh .= $silence;
+            }
+            if ($debug || $test) {
+                $c->writeln('Executing:');
+                $c->writeln($sh);
+            }
+            if (!$test) exec($sh);
+        } else {
+            $files = search_ext('(php|inc|js)');
+            $files = array_filter($files, create_function('$file', 'return substr($file, 0, 9) != "." . DS . "config" . DS;'));
+            $files = array_merge($files, search_ext('(php|dist)', 'config'));
+            $sh = $GLOBALS['xgettext'] . ' --language=' . $language .
+                  ' --keyword=_ --sort-output --force-po --copyright-holder="Horde Project"';
+            if ($gettext_version[0] > 0 || $gettext_version[1] > 11) {
+                $sh .= ' --msgid-bugs-address="dev@lists.horde.org"';
+            }
+            $file = 'po' . DS . $apps[$i] . '.pot';
+            if (file_exists($file) && !is_writable($file)) {
+                $c->writeln($c->red(sprintf('%s is not writable.', $file)));
+                footer();
+            }
+            $tmp_file = $file . '.tmp.pot';
+            $sh .= ' -o ' . $tmp_file . ' ' . implode(' ', $files) . ($debug ? '' : $silence);
+            if ($debug || $test) {
+                $c->writeln('Executing:');
+                $c->writeln($sh);
+            }
+            if (!$test) exec($sh);
+        }
+        if (file_exists($tmp_file)) {
+            $files = search_ext('html', 'templates');
+            if (!$test) $tmp = fopen($file . '.templates', 'w');
+            foreach ($files as $template) {
+                $fp = fopen($template, 'r');
+                $lineno = 0;
+                while (($line = fgets($fp, 4096)) !== false) {
+                    $lineno++;
+                    $offset = 0;
+                    while (($left = strpos($line, '<gettext>', $offset)) !== false) {
+                        $left += 9;
+                        $buffer = '';
+                        $linespan = 0;
+                        while (($end = strpos($line, '</gettext>', $left)) === false) {
+                            $buffer .= substr($line, $left);
+                            $left = 0;
+                            $line = fgets($fp, 4096);
+                            $linespan++;
+                            if ($line === false) {
+                                $c->writeln($c->red(sprintf("<gettext> tag not closed in file %s.\nOpening tag found in line %d.", $template, $lineno)));
+                                break 2;
+                            }
+                        }
+                        $buffer .= substr($line, $left, $end - $left);
+                        if (!$test) {
+                            fwrite($tmp, "#: $template:$lineno\n");
+                            fwrite($tmp, 'msgid "' . str_replace(array('"', "\n"), array('\"', "\\n\"\n\""), $buffer) . "\"\n");
+                            fwrite($tmp, 'msgstr ""' . "\n\n");
+                        }
+                        $offset = $end + 10;
+                    }
+                }
+                fclose($fp);
+            }
+            fclose($tmp);
+            $sh = $GLOBALS['msgcomm'] . " --more-than=0 --sort-output \"$tmp_file\" \"$file.templates\" --output-file \"$tmp_file\"" . $silence;
+            if ($debug || $test) {
+                $c->writeln('Executing:');
+                $c->writeln($sh);
+            }
+            if (!$test) {
+                exec($sh);
+                unlink($file . '.templates');
+            }
+            if (file_exists($file)) {
+                $diff = array_diff(file($tmp_file), file($file));
+                $diff = preg_grep('/^"POT-Creation-Date:/', $diff, PREG_GREP_INVERT);
+            }
+        }
+        if (!file_exists($file) || count($diff)) {
+            @unlink($file);
+            rename($tmp_file, $file);
+            $c->writeln($c->green('updated'));
+        } else {
+            @unlink($tmp_file);
+            $c->writeln($c->bold('not changed'));
+        }
+        chdir($curdir);
+    }
+}
+
+function merge()
+{
+    global $cmd_options, $apps, $dirs, $debug, $test, $c;
+
+    $compendium = ' --compendium="' . BASE . DS . 'po' . DS . 'compendium.po"';
+    foreach ($cmd_options[0] as $option) {
+        switch ($option[0]) {
+            case 'h':
+                usage();
+                footer();
+            case 'l':
+            case '--locale':
+                $lang = $option[1];
+                break;
+            case 'm':
+            case '--module':
+                $module = $option[1];
+                break;
+            case 'c':
+            case '--compendium':
+                $compendium = ' --compendium=' . $option[1];
+                break;
+            case 'n':
+            case '--no-compendium':
+                $compendium = '';
+                break;
+        }
+    }
+    if (!isset($lang) && !empty($compendium)) {
+        $c->writeln($c->red('Error: ' . 'No locale specified.'));
+        $c->writeln();
+        usage();
+        footer();
+    }
+
+    cleanup();
+
+    for ($i = 0; $i < count($dirs); $i++) {
+        if (!empty($module) && $module != $apps[$i]) {
+            continue;
+        }
+        $c->writeln(sprintf('Merging translation for module %s...', $c->bold($apps[$i])));
+        $dir = $dirs[$i] . DS . 'po' . DS;
+        if (empty($lang)) {
+            $langs = get_languages($dirs[$i]);
+        } else {
+            if (!@file_exists($dir . $lang . '.po')) {
+                $c->writeln('Skipped...');
+                $c->writeln();
+                continue;
+            }
+            $langs = array($lang);
+        }
+        foreach ($langs as $locale) {
+            $c->writeln(sprintf('Merging locale %s... ', $c->bold($locale)));
+            $sh = $GLOBALS['msgmerge'] . ' --update -v' . $compendium . ' "' . $dir . $locale . '.po" "' . $dir . $apps[$i] . '.pot"';
+            if ($debug || $test) {
+                $c->writeln('Executing:');
+                $c->writeln($sh);
+            }
+            if (!$test) exec($sh);
+            $c->writeln($c->green('done'));
+        }
+    }
+}
+
+function status()
+{
+    return;
+    global $cmd_options, $apps, $dirs, $debug, $test, $c;
+
+    $output = "status.html";
+    foreach ($cmd_options[0] as $option) {
+        switch ($option[0]) {
+            case 'h':
+                usage();
+                footer();
+            case 'l':
+            case '--locale':
+                $lang = $option[1];
+                break;
+            case 'm':
+            case '--module':
+                $module = $option[1];
+                break;
+            case 'o':
+            case '--output':
+                $output = $option[1];
+                break;
+       }
+    }
+    for ($i = 0; $i < count($dirs); $i++) {
+        if (!empty($module) && $module != $apps[$i]) {
+            continue;
+        }
+        $c->writeln(sprintf('Generating status for module %s...', $c->bold($apps[$i])));
+        if (empty($lang)) {
+            $langs = get_languages($dirs[$i]);
+        } else {
+            if (!@file_exists($dirs[$i] . DS . 'po' . DS . $lang . '.po')) {
+                $c->writeln('Skipped...');
+                $c->writeln();
+                continue;
+            }
+            $langs = array($lang);
+        }
+        foreach ($langs as $locale) {
+            $c->writeln(sprintf('Status for locale %s... ', $c->bold($locale)));
+        }
+    }
+}
+
+function compendium()
+{
+    global $cmd_options, $dirs, $debug, $test, $c, $silence;
+
+    $dir = BASE . DS . 'po' . DS;
+    $add = '';
+    foreach ($cmd_options[0] as $option) {
+        switch ($option[0]) {
+            case 'h':
+                usage();
+                footer();
+            case 'l':
+            case '--locale':
+                $lang = $option[1];
+                break;
+            case 'd':
+            case '--directory':
+                $dir = $option[1];
+                break;
+            case 'a':
+            case '--add':
+                $add = ' ' . $option[1];
+                break;
+        }
+    }
+    if (!isset($lang)) {
+        $c->writeln($c->red('Error: ' . 'No locale specified.'));
+        $c->writeln();
+        usage();
+        footer();
+    }
+    echo sprintf('Merging all %s.po files to the compendium... ', $lang);
+    $pofiles = array();
+    for ($i = 0; $i < count($dirs); $i++) {
+        $pofile = $dirs[$i] . DS . 'po' . DS . $lang . '.po';
+        if (file_exists($pofile)) {
+            $pofiles[] = $pofile;
+        }
+    }
+    if (!empty($dir) && substr($dir, -1) != DS) {
+        $dir .= DS;
+    }
+    $sh = $GLOBALS['msgcat'] . ' --sort-output ' . implode(' ', $pofiles) . $add . ' > ' . $dir . 'compendium.po ' . ($debug ? '' : $silence);
+    if ($debug || $test) {
+        $c->writeln();
+        $c->writeln('Executing:');
+        $c->writeln($sh);
+    }
+    if ($test) {
+        $ret = 0;
+    } else {
+        exec($sh, $out, $ret);
+    }
+    if ($ret == 0) {
+        $c->writeln($c->green('done'));
+    } else {
+        $c->writeln($c->red('failed'));
+    }
+}
+
+function init()
+{
+    global $cmd_options, $apps, $dirs, $debug, $test, $c, $silence;
+
+    foreach ($cmd_options[0] as $option) {
+        switch ($option[0]) {
+            case 'h':
+                usage();
+                footer();
+            case 'l':
+            case '--locale':
+                $lang = $option[1];
+                break;
+            case 'm':
+            case '--module':
+                $module = $option[1];
+                break;
+        }
+    }
+    if (empty($lang)) { $lang = getenv('LANG'); }
+    for ($i = 0; $i < count($dirs); $i++) {
+        if (!empty($module) && $module != $apps[$i]) { continue; }
+        $package = ucfirst($apps[$i]);
+        $package_u = String::upper($apps[$i]);
+        @include $dirs[$i] . '/lib/version.php';
+        $version = eval('return(defined("' . $package_u . '_VERSION") ? ' . $package_u . '_VERSION : "???");');
+        echo sprintf('Initializing module %s... ', $apps[$i]);
+        if (!@file_exists($dirs[$i] . '/po/' . $apps[$i] . '.pot')) {
+            $c->writeln($c->red('failed'));
+            $c->writeln(sprintf('%s not found. Run \'translation extract\' first.', $dirs[$i] . DS . 'po' . DS . $apps[$i] . '.pot'));
+            continue;
+        }
+        $dir = $dirs[$i] . DS . 'po' . DS;
+        $sh = $GLOBALS['msginit'] . ' --no-translator -i ' . $dir . $apps[$i] . '.pot ' .
+              (!empty($lang) ? ' -o ' . $dir . $lang . '.po --locale=' . $lang : '') .
+              ($debug ? '' : $silence);
+        if (!empty($lang) && !OS_WINDOWS) {
+            $pofile = $dirs[$i] . '/po/' . $lang . '.po';
+            $sh .= "; sed 's/PACKAGE package/$package package/' $pofile " .
+                   "| sed 's/PACKAGE VERSION/$package $version/' " .
+                   "| sed 's/messages for PACKAGE/messages for $package/' " .
+                   "| sed 's/Language-Team: none/Language-Team: i18n@lists.horde.org/' " .
+                   "> $pofile.tmp";
+        }
+        if ($debug || $test) {
+            $c->writeln('Executing:');
+            $c->writeln($sh);
+        }
+        if ($test) {
+            $ret = 0;
+        } else {
+            exec($sh, $out, $ret);
+        }
+        rename($pofile . '.tmp', $pofile);
+        if ($ret == 0) {
+            $c->writeln($c->green('done'));
+        } else {
+            $c->writeln($c->red('failed'));
+        }
+    }
+}
+
+function make()
+{
+    global $cmd_options, $apps, $dirs, $debug, $test, $c, $silence, $redir_err;
+
+    $compendium = BASE . DS . 'po' . DS . 'compendium.po';
+    $save_stats = false;
+    foreach ($cmd_options[0] as $option) {
+        switch ($option[0]) {
+            case 'h':
+                usage();
+                footer();
+            case 'l':
+            case '--locale':
+                $lang = $option[1];
+                break;
+            case 'm':
+            case '--module':
+                $module = $option[1];
+                break;
+            case 'c':
+            case '--compendium':
+                $compendium = $option[1];
+                break;
+            case 'n':
+            case '--no-compendium':
+                $compendium = '';
+                break;
+            case 's':
+            case '--statistics':
+                $save_stats = true;
+                break;
+        }
+    }
+    $horde = array_search('horde', $dirs);
+    $horde_msg = array();
+    $stats_array = array();
+
+    require_once 'Console/Table.php';
+    $stats = new Console_Table();
+    $stats->setHeaders(array('Module', 'Language', 'Translated', 'Fuzzy', 'Untranslated'));
+
+    for ($i = 0; $i < count($dirs); $i++) {
+        if (!empty($module) && $module != $apps[$i]) continue;
+        $c->writeln(sprintf('Building MO files for module %s...', $c->bold($apps[$i])));
+        if (empty($lang)) {
+            $langs = get_languages($dirs[$i]);
+        } else {
+            if (!@file_exists($dirs[$i] . DS . 'po' . DS . $lang . '.po')) {
+                $c->writeln('Skipped...');
+                $c->writeln();
+                continue;
+            }
+            $langs = array($lang);
+        }
+        foreach ($langs as $locale) {
+            $c->writeln(sprintf('Building locale %s... ', $c->bold($locale)));
+            $dir = $dirs[$i] . DS . 'locale' . DS . $locale . DS . 'LC_MESSAGES';
+            if (!is_dir($dir)) {
+                require_once 'System.php';
+                if ($debug) {
+                    $c->writeln(sprintf('Making directory %s', $dir));
+                }
+                if (!$test && !@System::mkdir("-p $dir")) {
+                    $c->writeln($c->red('Warning: ') . sprintf('Could not create locale directory for locale %s:', $locale));
+                    $c->writeln($dir);
+                    $c->writeln();
+                    continue;
+                }
+            }
+
+            /* Convert to unix linebreaks. */
+            $pofile = $dirs[$i] . DS . 'po' . DS . $locale . '.po';
+            $fp = fopen($pofile, 'r');
+            $content = fread($fp, filesize($pofile));
+            fclose($fp);
+
+            $content = str_replace("\r", '', $content);
+            $fp = fopen($pofile, 'wb');
+            fwrite($fp, $content);
+            fclose($fp);
+
+            /* Check PO file sanity. */
+            $sh = $GLOBALS['msgfmt'] . " --check \"$pofile\"$redir_err";
+            if ($debug || $test) {
+                $c->writeln('Executing:');
+                $c->writeln($sh);
+            }
+            if ($test) {
+                $ret = 0;
+            } else {
+                exec($sh, $out, $ret);
+            }
+            if ($ret != 0) {
+                $c->writeln($c->red('Warning: ') . 'an error has occured:');
+                $c->writeln(implode("\n", $out));
+                $c->writeln();
+                if ($apps[$i] == 'horde') {
+                    continue 2;
+                }
+                continue;
+            }
+
+            /* Compile MO file. */
+            $sh = $GLOBALS['msgfmt'] . ' --statistics -o "' . $dir . DS . $apps[$i] . '.mo" ';
+            if ($apps[$i] != 'horde') {
+                $horde_po = $dirs[$horde] . DS . 'po' . DS . $locale . '.po';
+                if (!@is_readable($horde_po)) {
+                    $c->writeln($c->red('Warning: ') . sprintf('the Horde PO file for the locale %s does not exist:', $locale));
+                    $c->writeln($horde_po);
+                    $c->writeln();
+                    $sh .= $dirs[$i] . DS . 'po' . DS . $locale . '.po';
+                } else {
+                    $sh = $GLOBALS['msgcomm'] . " --more-than=0 --sort-output \"$pofile\" \"$horde_po\" | $sh -";
+                }
+            } else {
+                $sh .= $pofile;
+            }
+            $sh .= $redir_err;
+            if ($debug || $test) {
+                $c->writeln('Executing:');
+                $c->writeln($sh);
+            }
+            $out = '';
+            if ($test) {
+                $ret = 0;
+            } else {
+                putenv('LANG=en');
+                exec($sh, $out, $ret);
+                putenv('LANG=' . $GLOBALS['language']);
+            }
+            if ($ret == 0) {
+                $c->writeln($c->green('done'));
+                $messages = array(0, 0, 0);
+                if (preg_match('/(\d+) translated/', $out[0], $match)) {
+                    $messages[0] = $match[1];
+                    if (isset($horde_msg[$locale])) {
+                        $messages[0] -= $horde_msg[$locale][0];
+                        if ($messages[0] < 0) $messages[0] = 0;
+                    }
+                }
+                if (preg_match('/(\d+) fuzzy/', $out[0], $match)) {
+                    $messages[1] = $match[1];
+                    if (isset($horde_msg[$locale])) {
+                        $messages[1] -= $horde_msg[$locale][1];
+                        if ($messages[1] < 0) $messages[1] = 0;
+                    }
+                }
+                if (preg_match('/(\d+) untranslated/', $out[0], $match)) {
+                    $messages[2] = $match[1];
+                    if (isset($horde_msg[$locale])) {
+                        $messages[2] -= $horde_msg[$locale][2];
+                        if ($messages[2] < 0) $messages[2] = 0;
+                    }
+                }
+                if ($apps[$i] == 'horde') {
+                    $horde_msg[$locale] = $messages;
+                }
+                $stats_array[$apps[$i]][$locale] = $messages;
+                $stats->addRow(array($apps[$i], $locale, $messages[0], $messages[1], $messages[2]));
+            } else {
+                $c->writeln($c->red('failed'));
+                exec($sh, $out, $ret);
+                $c->writeln(implode("\n", $out));
+            }
+            if (count($langs) > 1) {
+                continue;
+            }
+
+            /* Merge translation into compendium. */
+            if (!empty($compendium)) {
+                echo sprintf('Merging the PO file for %s to the compendium... ', $c->bold($apps[$i]));
+                if (!empty($dir) && substr($dir, -1) != DS) {
+                    $dir .= DS;
+                }
+                $sh = $GLOBALS['msgcat'] . " --sort-output \"$compendium\" \"$pofile\" > \"$compendium.tmp\"";
+                if (!$debug) {
+                    $sh .= $silence;
+                }
+                if ($debug || $test) {
+                    $c->writeln();
+                    $c->writeln('Executing:');
+                    $c->writeln($sh);
+                }
+                $out = '';
+                if ($test) {
+                    $ret = 0;
+                } else {
+                    exec($sh, $out, $ret);
+                }
+                @unlink($compendium);
+                rename($compendium . '.tmp', $compendium);
+                if ($ret == 0) {
+                    $c->writeln($c->green('done'));
+                } else {
+                    $c->writeln($c->red('failed'));
+                }
+            }
+            $c->writeln();
+        }
+    }
+    if (empty($module)) {
+        $c->writeln('Results:');
+    } else {
+        $c->writeln('Results (including Horde):');
+    }
+    $c->writeln($stats->getTable());
+    if ($save_stats) {
+        $fp = @fopen('translation_stats.txt', 'w');
+        if ($fp) {
+            fwrite($fp, serialize($stats_array));
+            fclose($fp);
+        }
+    }
+}
+
+function cleanup($keep_untranslated = false)
+{
+    global $cmd_options, $apps, $dirs, $debug, $test, $c;
+
+    foreach ($cmd_options[0] as $option) {
+        switch ($option[0]) {
+            case 'h':
+                usage();
+                footer();
+            case 'l':
+            case '--locale':
+                $lang = $option[1];
+                break;
+            case 'm':
+            case '--module':
+                $module = $option[1];
+                break;
+        }
+    }
+
+    for ($i = 0; $i < count($dirs); $i++) {
+        if (!empty($module) && $module != $apps[$i]) { continue; }
+        $c->writeln(sprintf('Cleaning up PO files for module %s...', $c->bold($apps[$i])));
+        if (empty($lang)) {
+            $langs = get_languages($dirs[$i]);
+        } else {
+            if (!@file_exists($dirs[$i] . DS . 'po' . DS . $lang . '.po')) {
+                $c->writeln('Skipped...');
+                $c->writeln();
+                continue;
+            }
+            $langs = array($lang);
+        }
+        foreach ($langs as $locale) {
+            $c->writeln(sprintf('Cleaning up locale %s... ', $c->bold($locale)));
+            $pofile = $dirs[$i] . DS . 'po' . DS . $locale . '.po';
+            $sh = $GLOBALS['msgattrib'] . ($keep_untranslated ? '' : ' --translated') . " --no-obsolete --no-fuzzy --force-po $pofile > $pofile.tmp";
+            if (!$debug) {
+                $sh .= $silence;
+            }
+            if ($debug || $test) {
+                $c->writeln();
+                $c->writeln('Executing:');
+                $c->writeln($sh);
+            }
+            $out = '';
+            if ($test) {
+                $ret = 0;
+            } else {
+                exec($sh, $out, $ret);
+            }
+            if ($ret == 0) {
+                @unlink($pofile);
+                rename($pofile . '.tmp', $pofile);
+                $c->writeln($c->green('done'));
+            } else {
+                @unlink($pofile . '.tmp', $pofile);
+                $c->writeln($c->red('failed'));
+            }
+            $c->writeln();
+        }
+    }
+}
+
+function commit($help_only = false)
+{
+    global $cmd_options, $apps, $dirs, $debug, $test, $c;
+
+    $docs = false;
+    foreach ($cmd_options[0] as $option) {
+        switch ($option[0]) {
+            case 'h':
+                usage();
+                footer();
+            case 'l':
+            case '--locale':
+                $lang = $option[1];
+                break;
+            case 'm':
+            case '--module':
+                $module = $option[1];
+                break;
+            case 'n':
+            case '--new':
+                $docs = true;
+                break;
+            case 'M':
+            case '--message':
+                $msg = $option[1];
+                break;
+        }
+    }
+    $files = array();
+    for ($i = 0; $i < count($dirs); $i++) {
+        if (!empty($module) && $module != $apps[$i]) continue;
+        if ($apps[$i] == 'horde') {
+            $dirs[] = $dirs[$i] . DS . 'admin';
+            $apps[] = 'horde/admin';
+            if (!empty($module)) {
+                $module = 'horde/admin';
+            }
+        }
+        if (empty($lang)) {
+            if ($help_only) {
+                $files = array_merge($files, strip_horde(search_ext('xml', $dirs[$i] . DS . 'locale')));
+            } else {
+                $files = array_merge($files, strip_horde(get_po_files($dirs[$i] . DS . 'po')));
+                $files = array_merge($files, strip_horde(search_file('^[a-z]{2}_[A-Z]{2}', $dirs[$i] . DS . 'locale', true)));
+            }
+        } else {
+            if ($help_only) {
+                if (!@file_exists($dirs[$i] . DS . 'locale' . DS . $lang . DS . 'help.xml')) continue;
+            } else {
+                if (!@file_exists($dirs[$i] . '/po/' . $lang . '.po')) continue;
+                $files[] = strip_horde($dirs[$i] . DS . 'po' . DS . $lang . '.po');
+            }
+            $files[] = strip_horde($dirs[$i] . DS . 'locale' . DS . $lang);
+        }
+        if ($docs && !$help_only && $apps[$i]) {
+            $files[] = strip_horde($dirs[$i] . DS . 'docs');
+            if ($apps[$i] == 'horde') {
+                $horde_conf = $dirs[array_search('horde', $dirs)] . DS . 'config' . DS;
+                $files[] = strip_horde($horde_conf . 'nls.php.dist');
+            }
+        }
+    }
+    chdir(BASE);
+    if (count($files)) {
+        if ($docs) {
+            $c->writeln('Adding new files to repository:');
+            $sh = 'cvs add';
+            foreach ($files as $file) {
+                if (strstr($file, 'locale') || strstr($file, '.po')) {
+                    $sh .= " $file";
+                    $c->writeln($file);
+                }
+            }
+            $sh .= '; cvs add';
+            foreach ($files as $file) {
+                if (strstr($file, 'locale')) {
+                    if ($help_only) {
+                        $sh .= ' ' . $file . DS . '*.xml';
+                        $c->writeln($file . DS . '*.xml');
+                    } else {
+                        $sh .= ' ' . $file . DS . '*.xml ' . $file . DS . 'LC_MESSAGES';
+                        $c->writeln($file . DS . "*.xml\n$file" . DS . 'LC_MESSAGES');
+                    }
+                }
+            }
+            if (!$help_only) {
+                $sh .= '; cvs add';
+                foreach ($files as $file) {
+                    if (strstr($file, 'locale')) {
+                        $add = $file . DS . 'LC_MESSAGES' . DS . '*.mo';
+                        $sh .= ' ' . $add;
+                        $c->writeln($add);
+                    }
+                }
+            }
+            $c->writeln();
+            if ($debug || $test) {
+                $c->writeln('Executing:');
+                $c->writeln($sh);
+            }
+            if (!$test) system($sh);
+            $c->writeln();
+        }
+        $c->writeln('Committing:');
+        $c->writeln(implode(' ', $files));
+        if (!empty($lang)) {
+            $lang = ' ' . $lang;
+        }
+        if (empty($msg)) {
+            if ($docs) {
+                $msg = "Add$lang translation.";
+            } elseif ($help_only) {
+                $msg = "Update$lang help file.";
+            } else {
+                $msg = "Update$lang translation.";
+            }
+        }
+        $sh = 'cvs commit -m "' . $msg . '" ' . implode(' ', $files);
+        if ($debug || $test) {
+            $c->writeln('Executing:');
+            $c->writeln($sh);
+        }
+        if (!$test) system($sh);
+    }
+}
+
+function update_help()
+{
+    global $cmd_options, $dirs, $apps, $debug, $test, $last_error_msg, $c;
+
+    foreach ($cmd_options[0] as $option) {
+        switch ($option[0]) {
+            case 'h':
+                usage();
+                footer();
+            case 'l':
+            case '--locale':
+                $lang = $option[1];
+                break;
+            case 'm':
+            case '--module':
+                $module = $option[1];
+                break;
+        }
+    }
+    $files = array();
+    for ($i = 0; $i < count($dirs); $i++) {
+        if (!empty($module) && $module != $apps[$i]) { continue; }
+        if (!is_dir("$dirs[$i]/locale")) continue;
+        if ($apps[$i] == 'horde') {
+            $dirs[] = $dirs[$i] . DS . 'admin';
+            $apps[] = 'horde/admin';
+            if (!empty($module)) {
+                $module = 'horde/admin';
+            }
+        }
+        if (empty($lang)) {
+            $files = search_file('help.xml', $dirs[$i] . DS . 'locale');
+        } else {
+            $files = array($dirs[$i] . DS . 'locale' . DS . $lang . DS . 'help.xml');
+        }
+        $file_en  = $dirs[$i] . DS . 'locale' . DS . 'en_US' . DS . 'help.xml';
+        if (!@file_exists($file_en)) {
+            $c->writeln(wordwrap($c->red('Warning: ') . sprintf('There doesn\'t yet exist a help file for %s.', $c->bold($apps[$i]))));
+            $c->writeln();
+            continue;
+        }
+        foreach ($files as $file_loc) {
+            $locale = substr($file_loc, 0, strrpos($file_loc, DS));
+            $locale = substr($locale, strrpos($locale, DS) + 1);
+            if ($locale == 'en_US') continue;
+            if (!@file_exists($file_loc)) {
+                $c->writeln(wordwrap($c->red('Warning: ') . sprintf('The %s help file for %s doesn\'t yet exist. Creating a new one.', $c->bold($locale), $c->bold($apps[$i]))));
+                $dir_loc = substr($file_loc, 0, -9);
+                if (!is_dir($dir_loc)) {
+                    require_once 'System.php';
+                    if ($debug || $test) {
+                        $c->writeln(sprintf('Making directory %s', $dir_loc));
+                    }
+                    if (!$test && !@System::mkdir("-p $dir_loc")) {
+                        $c->writeln($c->red('Warning: ') . sprintf('Could not create locale directory for locale %s:', $locale));
+                        $c->writeln($dir_loc);
+                        $c->writeln();
+                        continue;
+                    }
+                }
+                if ($debug || $test) {
+                    $c->writeln(wordwrap(sprintf('Copying %s to %s', $file_en, $file_loc)));
+                }
+                if (!$test && !@copy($file_en, $file_loc)) {
+                    $c->writeln($c->red('Warning: ') . sprintf('Could not copy %s to %s', $file_en, $file_loc));
+                }
+                $c->writeln();
+                continue;
+            }
+            $c->writeln(sprintf('Updating %s help file for %s.', $c->bold($locale), $c->bold($apps[$i])));
+            $fp = fopen($file_loc, 'r');
+            $line = fgets($fp);
+            fclose($fp);
+            if (!strstr($line, '<?xml')) {
+                $c->writeln(wordwrap($c->red('Warning: ') . sprintf('The help file %s didn\'t start with <?xml', $file_loc)));
+                $c->writeln();
+                continue;
+            }
+            $encoding = '';
+            if (preg_match('/encoding=(["\'])([^\\1]+)\\1/', $line, $match)) {
+                $encoding = $match[2];
+            }
+            $doc_en = domxml_open_file($file_en);
+            if (!is_object($doc_en)) {
+                $c->writeln(wordwrap($c->red('Warning: ') . sprintf('There was an error opening the file %s. Try running translation.php with the flag -d to see any error messages from the xml parser.', $file_en)));
+                $c->writeln();
+                continue 2;
+            }
+            $doc_loc = domxml_open_file($file_loc);
+            if (!is_object($doc_loc)) {
+                $c->writeln(wordwrap($c->red('Warning: ') . sprintf('There was an error opening the file %s. Try running translation.php with the flag -d to see any error messages from the xml parser.', $file_loc)));
+                $c->writeln();
+                continue;
+            }
+            $doc_new  = domxml_new_doc('1.0');
+            $help_en  = $doc_en->document_element();
+            $help_loc = $doc_loc->document_element();
+            $help_new = $help_loc->clone_node();
+            $entries_loc = array();
+            $entries_new = array();
+            $count_uptodate = 0;
+            $count_new      = 0;
+            $count_changed  = 0;
+            $count_unknown  = 0;
+            foreach ($doc_loc->get_elements_by_tagname('entry') as $entry) {
+                $entries_loc[$entry->get_attribute('id')] = $entry;
+            }
+            foreach ($doc_en->get_elements_by_tagname('entry') as $entry) {
+                $id = $entry->get_attribute('id');
+                if (array_key_exists($id, $entries_loc)) {
+                    if ($entries_loc[$id]->has_attribute('md5') &&
+                        md5($entry->get_content()) != $entries_loc[$id]->get_attribute('md5')) {
+                        $comment = $doc_loc->create_comment(" English entry:\n" . str_replace('--', '&#45;&#45;', $doc_loc->dump_node($entry)));
+                        $entries_loc[$id]->append_child($comment);
+                        $entry_new = $entries_loc[$id]->clone_node(true);
+                        $entry_new->set_attribute('state', 'changed');
+                        $count_changed++;
+                    } else {
+                        if (!$entries_loc[$id]->has_attribute('state')) {
+                            $comment = $doc_loc->create_comment(" English entry:\n" . str_replace('--', '&#45;&#45;', $doc_loc->dump_node($entry)));
+                            $entries_loc[$id]->append_child($comment);
+                            $entry_new = $entries_loc[$id]->clone_node(true);
+                            $entry_new->set_attribute('state', 'unknown');
+                            $count_unknown++;
+                        } else {
+                            $entry_new = $entries_loc[$id]->clone_node(true);
+                            $count_uptodate++;
+                        }
+                    }
+                } else {
+                    $entry_new = $entry->clone_node(true);
+                    $entry_new->set_attribute('state', 'new');
+                    $count_new++;
+                }
+                $entries_new[] = $entry_new;
+            }
+            $doc_new->append_child($doc_new->create_comment(' $' . 'Horde$ '));
+            foreach ($entries_new as $entry) {
+                $help_new->append_child($entry);
+            }
+            $c->writeln(wordwrap(sprintf('Entries: %d total, %d up-to-date, %d new, %d changed, %d unknown',
+                                     $count_uptodate + $count_new + $count_changed + $count_unknown,
+                                     $count_uptodate, $count_new, $count_changed, $count_unknown)));
+            $doc_new->append_child($help_new);
+            $output = $doc_new->dump_mem(true, $encoding);
+            if ($debug || $test) {
+                $c->writeln(wordwrap(sprintf('Writing updated help file to %s.', $file_loc)));
+            }
+            if (!$test) {
+                $fp = fopen($file_loc, 'w');
+                $line = fwrite($fp, $output);
+                fclose($fp);
+            }
+            $c->writeln(sprintf('%d bytes written.', strlen($output)));
+            $c->writeln();
+        }
+    }
+}
+
+function make_help()
+{
+    global $cmd_options, $dirs, $apps, $debug, $test, $c;
+
+    foreach ($cmd_options[0] as $option) {
+        switch ($option[0]) {
+            case 'h':
+                usage();
+                footer();
+            case 'l':
+            case '--locale':
+                $lang = $option[1];
+                break;
+            case 'm':
+            case '--module':
+                $module = $option[1];
+                break;
+        }
+    }
+    $files = array();
+    for ($i = 0; $i < count($dirs); $i++) {
+        if (!empty($module) && $module != $apps[$i]) continue;
+        if (!is_dir("$dirs[$i]/locale")) continue;
+        if ($apps[$i] == 'horde') {
+            $dirs[] = $dirs[$i] . DS . 'admin';
+            $apps[] = 'horde/admin';
+            if (!empty($module)) {
+                $module = 'horde/admin';
+            }
+        }
+        if (empty($lang)) {
+            $files = search_file('help.xml', $dirs[$i] . DS . 'locale');
+        } else {
+            $files = array($dirs[$i] . DS . 'locale' . DS . $lang . DS . 'help.xml');
+        }
+        $file_en  = $dirs[$i] . DS . 'locale' . DS . 'en_US' . DS . 'help.xml';
+        if (!@file_exists($file_en)) {
+            continue;
+        }
+        foreach ($files as $file_loc) {
+            if (!@file_exists($file_loc)) {
+                $c->writeln('Skipped...');
+                $c->writeln();
+                continue;
+            }
+            $locale = substr($file_loc, 0, strrpos($file_loc, DS));
+            $locale = substr($locale, strrpos($locale, DS) + 1);
+            if ($locale == 'en_US') continue;
+            $c->writeln(sprintf('Updating %s help file for %s.', $c->bold($locale), $c->bold($apps[$i])));
+            $fp = fopen($file_loc, 'r');
+            $line = fgets($fp);
+            fclose($fp);
+            if (!strstr($line, '<?xml')) {
+                $c->writeln(wordwrap($c->red('Warning: ') . sprintf('The help file %s didn\'t start with <?xml', $file_loc)));
+                $c->writeln();
+                continue;
+            }
+            $encoding = '';
+            if (preg_match('/encoding=(["\'])([^\\1]+)\\1/', $line, $match)) {
+                $encoding = $match[2];
+            }
+            $doc_en   = domxml_open_file($file_en);
+            if (!is_object($doc_en)) {
+                $c->writeln(wordwrap($c->red('Warning: ') . sprintf('There was an error opening the file %s. Try running translation.php with the flag -d to see any error messages from the xml parser.', $file_en)));
+                $c->writeln();
+                continue 2;
+            }
+            $doc_loc  = domxml_open_file($file_loc);
+            if (!is_object($doc_loc)) {
+                $c->writeln(wordwrap($c->red('Warning: ') . sprintf('There was an error opening the file %s. Try running translation.php with the flag -d to see any error messages from the xml parser.', $file_loc)));
+                $c->writeln();
+                continue;
+            }
+            $help_loc = $doc_loc->document_element();
+            $md5_en   = array();
+            $count_all = 0;
+            $count     = 0;
+            foreach ($doc_en->get_elements_by_tagname('entry') as $entry) {
+                $md5_en[$entry->get_attribute('id')] = md5($entry->get_content());
+            }
+            foreach ($doc_loc->get_elements_by_tagname('entry') as $entry) {
+                foreach ($entry->child_nodes() as $child) {
+                    if ($child->node_type() == XML_COMMENT_NODE && strstr($child->node_value(), 'English entry')) {
+                        $entry->remove_child($child);
+                    }
+                }
+                $count_all++;
+                $id = $entry->get_attribute('id');
+                if (!array_key_exists($id, $md5_en)) {
+                    $c->writeln(wordwrap($c->red('Warning: ') . sprintf('No entry with the id "%s" exists in the original help file.', $id)));
+                } else {
+                    $entry->set_attribute('md5', $md5_en[$id]);
+                    $entry->set_attribute('state', 'uptodate');
+                    $count++;
+                }
+            }
+            $output = $doc_loc->dump_mem(true, $encoding);
+            if (!$test) {
+                $fp = fopen($file_loc, 'w');
+                $line = fwrite($fp, $output);
+                fclose($fp);
+            }
+            $c->writeln(sprintf('%d of %d entries marked as up-to-date', $count, $count_all));
+            $c->writeln();
+        }
+    }
+}
+
+$curdir = getcwd();
+define('DS', DIRECTORY_SEPARATOR);
+
+$language = getenv('LANG');
+if (empty($language)) {
+    $language = getenv('LANGUAGE');
+}
+
+@define('HORDE_BASE', dirname(__FILE__) . '/../..');
+require_once HORDE_BASE . '/lib/core.php';
+require_once 'Horde/CLI.php';
+
+$c = &new Horde_CLI();
+if (!$c->runningFromCLI()) {
+    $c->fatal('This script must be run from the command line.');
+}
+$c->init();
+
+require HORDE_BASE . '/config/nls.php';
+require_once 'Horde/NLS.php';
+if (!empty($language)) {
+    $tmp = explode('.', $language);
+    $language = $tmp[0];
+    $language = NLS::_map(trim($language));
+    if (!NLS::isValid($language)) {
+        $language = NLS::_map(substr($language, 0, 2));
+    }
+    if (NLS::isValid($language)) {
+        setlocale(LC_ALL, $language);
+        bindtextdomain('horde', HORDE_BASE . '/locale');
+        textdomain('horde');
+        if (array_key_exists(1, $tmp) &&
+            function_exists('bind_textdomain_codeset')) {
+            bind_textdomain_codeset('horde', $tmp[1]);
+        }
+    }
+}
+
+$c->writeln($c->bold('---------------------------'));
+$c->writeln($c->bold('Horde translation generator'));
+$c->writeln($c->bold('---------------------------'));
+
+/* Sanity checks */
+if (!extension_loaded('gettext')) {
+    $c->writeln($c->red('Gettext extension not found!'));
+    footer();
+}
+
+$c->writeln('Loading libraries...');
+$libs_found = true;
+
+foreach (array('Console_Getopt' => 'Console/Getopt.php',
+               'Console_Table'  => 'Console/Table.php',
+               'File_Find'      => 'File/Find.php')
+         as $class => $file) {
+    echo $class . '... ';
+    @include_once $file;
+    if (class_exists($class)) {
+        $c->writeln($c->green('OK'));
+    } else {
+        $c->writeln($c->red(sprintf('%s not found.', $class)));
+        $libs_found = false;
+    }
+}
+
+if (!$libs_found) {
+    $c->writeln();
+    $c->writeln('Make sure that you have PEAR installed and in your include path.');
+    $c->writeln('include_path: ' . ini_get('include_path'));
+    footer();
+}
+$c->writeln();
+
+/* Commandline parameters */
+$args    = Console_Getopt::readPHPArgv();
+$options = Console_Getopt::getopt($args, 'b:dht', array('base=', 'debug', 'help', 'test'));
+if (PEAR::isError($options) && $args[0] == $_SERVER['PHP_SELF']) {
+    array_shift($args);
+    $options = Console_Getopt::getopt($args, 'b:dht', array('base=', 'debug', 'help', 'test'));
+}
+if (PEAR::isError($options)) {
+    $c->writeln($c->red('Getopt Error: ' . str_replace('Console_Getopt:', '', $options->getMessage())));
+    $c->writeln();
+    usage();
+    footer();
+}
+if (empty($options[1][0])) {
+    $c->writeln($c->red('Error: ' . 'No command specified.'));
+    $c->writeln();
+    usage();
+    footer();
+}
+$debug = false;
+$test  = false;
+foreach ($options[0] as $option) {
+    switch ($option[0]) {
+        case 'b':
+        case '--base':
+            if (substr($option[1], -1) == DS) {
+                $option[1] = substr($option[1], 0, -1);
+            }
+            define('BASE', $option[1]);
+            break;
+        case 'd':
+        case '--debug':
+            $debug = true;
+            break;
+        case 't':
+        case '--test':
+            $test = true;
+            break;
+        case 'h':
+        case '--help':
+            usage();
+            footer();
+    }
+}
+if (!$debug) {
+    ini_set('error_reporting', false);
+}
+if (!defined('BASE')) {
+    define('BASE', HORDE_BASE);
+}
+if ($options[1][0] == 'help') {
+    usage();
+    footer();
+}
+$silence = $debug || OS_WINDOWS ? '' : ' 2> /dev/null';
+$redir_err = OS_WINDOWS ? '' : ' 2>&1';
+$options_list = array(
+    'cleanup'    => array('hl:m:', array('module=', 'locale=')),
+    'commit'     => array('hl:m:nM:', array('module=', 'locale=', 'new', 'message=')),
+    'commit-help'=> array('hl:m:nM:', array('module=', 'locale=', 'new', 'message=')),
+    'compendium' => array('hl:d:a:', array('locale=', 'directory=', 'add=')),
+    'extract'    => array('hm:', array('module=')),
+    'init'       => array('hl:m:nc:', array('module=', 'locale=', 'no-compendium', 'compendium=')),
+    'merge'      => array('hl:m:c:n', array('module=', 'locale=', 'compendium=', 'no-compendium')),
+    'make'       => array('hl:m:c:ns', array('module=', 'locale=', 'compendium=', 'no-compendium', 'statistics')),
+    'make-help'  => array('hl:m:', array('module=', 'locale=')),
+    'update'     => array('hl:m:c:n', array('module=', 'locale=', 'compendium=', 'no-compendium')),
+    'update-help'=> array('hl:m:', array('module=', 'locale=')),
+    'status'     => array('hl:m:o:', array('module=', 'locale=', 'output='))
+);
+$options_arr = $options[1];
+$cmd         = array_shift($options_arr);
+if (array_key_exists($cmd, $options_list)) {
+    $cmd_options = Console_Getopt::getopt($options_arr, $options_list[$cmd][0], $options_list[$cmd][1]);
+    if (PEAR::isError($cmd_options)) {
+        $c->writeln($c->red('Error: ' . str_replace('Console_Getopt:', '', $cmd_options->getMessage())));
+        $c->writeln();
+        usage();
+        footer();
+    }
+}
+
+/* Searching applications */
+check_binaries();
+
+$c->writeln(sprintf('Searching Horde applications in %s', BASE));
+$dirs = search_applications();
+
+if ($debug) {
+    $c->writeln('Found directories:');
+    $c->writeln(implode("\n", $dirs));
+}
+
+$apps = strip_horde($dirs);
+$apps[0] = 'horde';
+$c->writeln(wordwrap(sprintf('Found applications: %s', implode(', ', $apps))));
+$c->writeln();
+
+switch ($cmd) {
+    case 'cleanup':
+    case 'commit':
+    case 'compendium':
+    case 'merge':
+        $cmd();
+        break;
+    case 'commit-help':
+        commit(true);
+        break;
+    case 'extract':
+        xtract();
+        break;
+    case 'init':
+        init();
+        $c->writeln();
+        merge();
+        break;
+    case 'make':
+        cleanup(true);
+        $c->writeln();
+        make();
+        break;
+    case 'make-help':
+        make_help();
+        break;
+    case 'update':
+        xtract();
+        $c->writeln();
+        merge();
+        break;
+    case 'update-help':
+        update_help();
+        break;
+    case 'status':
+//        xtract();
+        merge();
+//        status();
+        break;
+    default:
+        $c->writeln($c->red('Error: ') . sprintf('Unknown command: %s', $cmd));
+        $c->writeln();
+        usage();
+        footer();
+}
+
+footer();
diff --git a/babel/stats.php b/babel/stats.php
new file mode 100644 (file)
index 0000000..fb7a254
--- /dev/null
@@ -0,0 +1,120 @@
+<?php
+/**
+ * Copyright 2000-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author  Joel Vandal <joel@scopserv.com>
+ * @package Babel
+ */
+
+@define('BABEL_BASE', dirname(__FILE__)) ;
+require_once BABEL_BASE . '/lib/base.php';
+
+if ($app) {
+    /* Render the page. */
+    Translation::RB_init();
+}
+require BABEL_TEMPLATES . '/common-header.inc';
+
+if ($app) {
+    Translation::RB_start(30);
+}
+echo $template->fetch(BABEL_TEMPLATES . '/layout.html');
+
+$vars = &Variables::getDefaultVariables();
+
+/* Create upload form */
+$form = &new Horde_Form($vars, _("View Statistics"), 'stats');
+
+if (!$app) {
+    $form->setButtons(_("View"));
+    $form->addVariable(_("Module"), 'module', 'enum', true, false, null, array(Translation::listApps(true), true));
+    $form->addVariable('', '', 'spacer', true);
+    
+    $renderer_params = array();
+    $renderer = &new Horde_Form_Renderer($renderer_params);
+    $renderer->setAttrColumnWidth('20%');
+    
+    $form->renderActive($renderer, $vars, Horde::selfURL(), 'post');
+} else {
+    Translate_Display::header(_("Horde translation generator"));
+    
+    $dirs = Translate::search_applications();
+    $apps = Translate::strip_horde($dirs);
+    $apps[0] = 'horde';
+    Translate_Display::info();
+    
+    foreach($dirs as $d => $dir) {
+       $dir = realpath($dir);
+       $pofile  = $dir . '/po/' . $lang . '.po';
+       
+       if (!@file_exists($pofile)) {
+           continue;
+       }
+
+       $_app = str_replace(realpath(HORDE_BASE), '', $dir);
+       $_app = str_replace('/', '', $_app);
+       if (empty($_app)) {
+           $_app = 'horde';
+       }
+       
+       if ($app != 'ALL' && $app != $_app) {
+           continue;
+       }
+
+       if (!Translation::hasPermission("module:$_app")) {
+           continue;
+       }
+       
+       Translate_Display::header($_app);
+       
+       $report = Translate::stats($_app);
+       
+       echo '<table width="100%" align="center" border="0" cellspacing="0" cellpadding="0">';
+       echo '<tr class="control">';
+       echo '<td class="control" style="border-bottom: 1px solid #999;"><b>' . _("Language") . '</b></td>';
+       echo '<td width="5%"><b>' . _("Locale") . '</b></td>';
+       echo '<td width="15%"><b>' . _("Status") . '</b></td>';
+       echo '<td valign="bottom" style="width: 80px;"><b>' . _("Translated") . '</b></td>';
+       echo '<td valign="bottom" style="width: 80px;"><b>' . _("Fuzzy") . '</b></td>';
+       echo '<td valign="bottom" style="width: 80px;"><b>' . _("Untranslated") . '</b></td>';
+       echo '<td valign="bottom" style="width: 80px;"><b>' . _("Obsolete") . '</b></td>';
+       echo '</tr>';   
+       
+       $i = 0;
+       $j = 0;
+       $line = 0;
+       $last_key = null;
+       foreach ($report as $key => $value) {
+           
+           if (!Translation::hasPermission("language:$key")) {
+               continue;
+           }
+
+           if ($key == $_SESSION['translation']['language']) {
+               echo "\n<tr class=\"smallheader control\">";
+           } else {
+               echo "\n<tr class=\"item" . ($i++ % 2) . "\">";
+           }
+           echo "\n\t<td>" . $nls['languages'][$key] . "</td>";
+           echo "\n\t<td>" . Horde::link(Util::addParameter(Horde::applicationUrl('view.php'), array('display_language' => $key, 'module' => $_app))) . $key . '</a>' . "</td>";
+           echo "\n\t<td>" . Translate_Display::create_bargraph($value[2], $value[0]) . "</td>";
+           echo "\n\t<td>" . $value[2] . "</td>";
+           echo "\n\t<td>" . $value [3] . "</td>";
+           echo "\n\t<td>" . $value[4] . "</td>";
+           echo "\n\t<td>" . $value[5] . "</td>";
+           echo "\t</tr>";
+           $last_key = $key;
+       }
+       
+       echo '</table>';
+       
+       Translate_Display::info();
+    }
+    
+    Translation::RB_close();
+}
+
+require $registry->get('templates', 'horde') . '/common-footer.inc';
diff --git a/babel/templates/common-header.inc b/babel/templates/common-header.inc
new file mode 100644 (file)
index 0000000..a99bc08
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+if (isset($language)) {
+    header('Content-type: text/html; charset=' . NLS::getCharset());
+    header('Vary: Accept-Language');
+}
+?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
+<?php echo !empty($language) ? '<html lang="' . strtr($language, '_', '-') . '">' : '<html>' ?>
+<head>
+<?php
+
+$page_title = $GLOBALS['registry']->get('name');
+if (!empty($title)) $page_title .= ' :: ' . $title;
+if (!empty($refresh_time) && ($refresh_time > 0) && !empty($refresh_url)) {
+    echo "<meta http-equiv=\"refresh\" content=\"$refresh_time;url=$refresh_url\">\n";
+}
+
+Horde::includeScriptFiles();
+
+?>
+<title><?php echo $page_title ?></title>
+<?php echo Horde::stylesheetLink('translation') ?>
+</head>
+
+<body<?php if (Util::nonInputVar('bodyClass')) echo ' class="' . $bodyClass . '"' ?>>
diff --git a/babel/templates/index.php b/babel/templates/index.php
new file mode 100644 (file)
index 0000000..18fc496
--- /dev/null
@@ -0,0 +1,78 @@
+<table style="width:100%" cellspacing="0">
+<tr class="header">
+<td style="width:200px; padding-left:8px; vertical-align:top">
+ <h1 class="header">
+  <gettext>Command</gettext>
+ </h1>
+</td>
+<td style="padding-left:8px; vertical-align:top">
+ <h1 class="header">
+  <gettext>Description</gettext>
+ </h1>
+</td>
+
+<?php 
+
+$cmds = array();
+
+$cmds['view'] = array(
+  'desc' => _("View"),
+  'text' => _("View all translations."),
+  'url'  => Horde::applicationUrl('view.php')
+);
+
+$cmds['download'] = array(
+  'desc' => _("Download"),
+  'text' => _("Download all current PO files."),
+  'url'  => Horde::applicationUrl('download.php')
+);
+
+$cmds['upload'] = array(
+  'desc' => _("Upload"),
+  'text' => _("Upload new PO files."),
+  'url'  => Horde::applicationUrl('upload.php')
+);
+
+$cmds['stats'] = array(
+  'desc' => _("Statistics"),
+  'text' => _("Get statistics about translations."),
+  'url'  => Horde::applicationUrl('stats.php')
+);
+
+$cmds['extract'] = array(
+  'desc' => _("Extract"),
+  'text' => _("Generate and merge PO files."),
+  'url'  => Horde::applicationUrl('extract.php')
+);
+
+$cmds['make'] = array(
+  'desc' => _("Make"),
+  'text' => _("Build binary MO files from the specified PO files."),
+  'url'  => Horde::applicationUrl('make.php')
+);
+
+$cmds['commit'] = array(
+  'desc' => _("Commit"),
+  'text' => _("Commit translations to the SVN server."),
+  'url'  => Horde::applicationUrl('commit.php')
+);
+
+$cmds['reset'] = array(
+  'desc' => _("Reset"),
+  'text' => _("The reset procedure will delete all PO files from the server for all modules and restore from SVN server."),
+  'url'  => Horde::applicationUrl('reset.php')
+);
+
+
+$i = 0;
+
+foreach($cmds as $cmdid => $cmd) {
+    if (Translation::hasPermission($cmdid)) {
+       echo '<tr height="50" class="item' . ($i++ % 2) . '">';
+       echo '<td align="center"><div style="width: 140px;" onclick="window.location=\'' . $cmd['url'] . '\';" class="button"><a class="smallheader" href="' . $cmd['url'] . '">' . $cmd['desc'] . '</a></div></td>';
+       echo '<td>' . $cmd['text'] . '</td>';
+       echo '</tr>';
+    }
+}
+?>
+</table>
diff --git a/babel/templates/layout.html b/babel/templates/layout.html
new file mode 100644 (file)
index 0000000..ec84ebc
--- /dev/null
@@ -0,0 +1,14 @@
+<div id="menu">
+ <span style="float:right">
+  <tag:fmenu />
+ </span>
+ <tag:menu />
+</div>
+<br />
+
+<tag:notify />
+
+<table cellspacing=0 cellpadding=0 width=100%>
+<tr><td class="header control"><tag:lang /></td></tr>
+</table>
+<br />
diff --git a/babel/themes/graphics/az.png b/babel/themes/graphics/az.png
new file mode 100644 (file)
index 0000000..1b9367d
Binary files /dev/null and b/babel/themes/graphics/az.png differ
diff --git a/babel/themes/graphics/checked.gif b/babel/themes/graphics/checked.gif
new file mode 100644 (file)
index 0000000..56cacd3
Binary files /dev/null and b/babel/themes/graphics/checked.gif differ
diff --git a/babel/themes/graphics/config.png b/babel/themes/graphics/config.png
new file mode 100644 (file)
index 0000000..8953756
Binary files /dev/null and b/babel/themes/graphics/config.png differ
diff --git a/babel/themes/graphics/delete.png b/babel/themes/graphics/delete.png
new file mode 100644 (file)
index 0000000..ce480ec
Binary files /dev/null and b/babel/themes/graphics/delete.png differ
diff --git a/babel/themes/graphics/down.png b/babel/themes/graphics/down.png
new file mode 100644 (file)
index 0000000..9fda51c
Binary files /dev/null and b/babel/themes/graphics/down.png differ
diff --git a/babel/themes/graphics/edit.png b/babel/themes/graphics/edit.png
new file mode 100644 (file)
index 0000000..2a3cd5a
Binary files /dev/null and b/babel/themes/graphics/edit.png differ
diff --git a/babel/themes/graphics/extract.png b/babel/themes/graphics/extract.png
new file mode 100644 (file)
index 0000000..980b9fe
Binary files /dev/null and b/babel/themes/graphics/extract.png differ
diff --git a/babel/themes/graphics/list.png b/babel/themes/graphics/list.png
new file mode 100644 (file)
index 0000000..c5bfaf3
Binary files /dev/null and b/babel/themes/graphics/list.png differ
diff --git a/babel/themes/graphics/locked.png b/babel/themes/graphics/locked.png
new file mode 100644 (file)
index 0000000..da2752f
Binary files /dev/null and b/babel/themes/graphics/locked.png differ
diff --git a/babel/themes/graphics/make.png b/babel/themes/graphics/make.png
new file mode 100644 (file)
index 0000000..734689e
Binary files /dev/null and b/babel/themes/graphics/make.png differ
diff --git a/babel/themes/graphics/redbox_spinner.gif b/babel/themes/graphics/redbox_spinner.gif
new file mode 100644 (file)
index 0000000..4eeb8b2
Binary files /dev/null and b/babel/themes/graphics/redbox_spinner.gif differ
diff --git a/babel/themes/graphics/sample.png b/babel/themes/graphics/sample.png
new file mode 100644 (file)
index 0000000..57fc33d
Binary files /dev/null and b/babel/themes/graphics/sample.png differ
diff --git a/babel/themes/graphics/skeleton.png b/babel/themes/graphics/skeleton.png
new file mode 100644 (file)
index 0000000..af7b9ff
Binary files /dev/null and b/babel/themes/graphics/skeleton.png differ
diff --git a/babel/themes/graphics/translation.png b/babel/themes/graphics/translation.png
new file mode 100644 (file)
index 0000000..63ce64d
Binary files /dev/null and b/babel/themes/graphics/translation.png differ
diff --git a/babel/themes/graphics/unchecked.gif b/babel/themes/graphics/unchecked.gif
new file mode 100644 (file)
index 0000000..bb87f1c
Binary files /dev/null and b/babel/themes/graphics/unchecked.gif differ
diff --git a/babel/themes/graphics/up.png b/babel/themes/graphics/up.png
new file mode 100644 (file)
index 0000000..5cc343c
Binary files /dev/null and b/babel/themes/graphics/up.png differ
diff --git a/babel/themes/graphics/upload.png b/babel/themes/graphics/upload.png
new file mode 100644 (file)
index 0000000..e1cce6d
Binary files /dev/null and b/babel/themes/graphics/upload.png differ
diff --git a/babel/themes/graphics/view.png b/babel/themes/graphics/view.png
new file mode 100644 (file)
index 0000000..c5bfaf3
Binary files /dev/null and b/babel/themes/graphics/view.png differ
diff --git a/babel/themes/graphics/za.png b/babel/themes/graphics/za.png
new file mode 100644 (file)
index 0000000..a4577f2
Binary files /dev/null and b/babel/themes/graphics/za.png differ
diff --git a/babel/themes/screen.css b/babel/themes/screen.css
new file mode 100644 (file)
index 0000000..f263e68
--- /dev/null
@@ -0,0 +1,45 @@
+/* Redbox styles. */
+#RB_overlay {
+    position: absolute;
+    z-index: 100;
+    width: 100%;
+    height: 100%;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    min-height: 100%;
+    background-color: #000;
+    opacity: .6;
+    filter: alpha(opacity=60);
+}
+#RB_loading {
+    z-index: 101;
+    width: 66;
+    margin-left: auto;
+    margin-right: auto;
+    margin-top: 200px;
+    padding-bottom: 66px;
+    text-align: center;
+    background: url("graphics/redbox_spinner.gif") no-repeat bottom center;
+}
+#RB_window {
+    z-index: 102;
+    background-color: #fff;
+    display: block;
+    text-align: left;
+    overflow: hidden;
+    margin: 20px auto 0 auto;
+    position: fixed;
+    position: absolute;
+}
+
+#RB_confirm {
+    width: 20em;
+    padding: 1em;
+    border: 1px solid #ccc;
+    background: #ffc;
+}
+#RB_confirm input {
+    margin: .2em;
+}
diff --git a/babel/upload.php b/babel/upload.php
new file mode 100644 (file)
index 0000000..37fd38d
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+/**
+ * Copyright 2000-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author  Joel Vandal <joel@scopserv.com>
+ * @package Babel
+ */
+
+@define('BABEL_BASE', dirname(__FILE__)) ;
+require_once BABEL_BASE . '/lib/base.php';
+
+$vars = &Variables::getDefaultVariables();
+
+/* Create upload form */
+$form = &new Horde_Form($vars, _("Upload new Translation"), 'upload');
+$form->setButtons(_("Upload"));
+$form->addVariable(_("Module"), 'module', 'enum', true, false, null, array(Translation::listApps(true), true));
+$form->addVariable(_("Translations File (.PO)"), 'po_file', 'file', true, false);
+$form->addVariable('', '', 'spacer', true);
+
+/* Validate form if submitted */
+if (Util::getFormData('submitbutton') == _("Upload")) {
+    if ($form->validate($vars, false)) {
+       $form->getInfo($vars, $form_values);
+       
+       $po_module = @$form_values['module'];
+       if (empty($po_module)) {
+           $notification->push(_("Please select module of translations PO file!"), 'horde.error');
+       } else {
+           $po_file_path = @$form_values['po_file']['file'];
+           $po_file_name = @$form_values['po_file']['name'];
+           $po_file_size = @$form_values['po_file']['size'];
+           if (empty($po_file_path) || substr($po_file_name, -3) != '.po' || $po_file_size <= 0) {
+               $notification->push(_("Invalid Translations file. Please submit a valid PO file!"), 'horde.error');
+           } else {
+               if ($po_module == 'horde') {
+                   $mod = '';
+               } else {
+                   $mod = $po_module;
+               }
+               
+               $notification->push(sprintf(_("Upload successful for %s (%s)"), $po_module, $lang), 'horde.success');
+               $cmd = "cp $po_file_path " . HORDE_BASE . "/$mod/po/$lang.po";
+               system($cmd);
+               
+               $url = Horde::applicationUrl('upload.php');
+               // Redirect to page URL
+               header('Location: ' . $url);
+               exit;
+           }
+       }
+    }
+}
+
+/* Render upload page */
+require BABEL_TEMPLATES . '/common-header.inc';
+echo $template->fetch(BABEL_TEMPLATES . '/layout.html');
+
+$renderer_params = array();
+$renderer = &new Horde_Form_Renderer($renderer_params);
+$renderer->setAttrColumnWidth('20%');
+
+$form->renderActive($renderer, $vars, Horde::selfURL(), 'post');
+
+require $registry->get('templates', 'horde') . '/common-footer.inc';
diff --git a/babel/view.php b/babel/view.php
new file mode 100644 (file)
index 0000000..4f60aa9
--- /dev/null
@@ -0,0 +1,502 @@
+<?php
+/**
+ * Copyright 2000-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author  Joel Vandal <joel@scopserv.com>
+ * @package Babel
+ */
+
+require_once dirname(__FILE__) . '/lib/base.php';
+
+require_once 'Horde/Lock.php';
+
+$meta_params = array(
+                    "Project-Id-Version" => @$_SESSION['translation']['language'],
+                    "Report-Msgid-Bugs-To" => "support@scopserv.com",
+                    "POT-Creation-Date" => "",
+                    "PO-Revision-Date" => "",
+                    "Last-Translator" => "",
+                    "Language-Team" => "",
+                    "MIME-Version" => "1.0",
+                    "Content-Type" => "text/plain; charset=utf-8",
+                    "Content-Transfer-Encoding" => "8bit",
+                    "Plural-Forms" => "nplurals=2; plural=(n > 1);");
+
+$app      = Util::getFormData('module');
+$editmode = Util::getFormData('editmode', 0);
+$cstring  = Util::getFormData('cstring');
+$page     = Util::getFormData('page', 0);
+$filter   = Util::getFormData('filter');
+$search   = Util::getFormData('search');
+
+if ($app) {
+    /* Render the page. */
+    Translation::RB_init();
+}
+
+/* Render the page. */
+require BABEL_TEMPLATES . '/common-header.inc';
+
+if ($app) {
+    if ($editmode) {
+       Translation::RB_start(10);
+    } else {
+       Translation::RB_start(60);
+    }
+}
+
+echo $template->fetch(BABEL_TEMPLATES . '/layout.html');
+
+$app = Util::getFormData('module');
+$show = 'edit';
+$vars = &Variables::getDefaultVariables();
+
+if ($app) {
+    
+    $napp = ($app == 'horde') ? '' : $app;
+    $pofile = HORDE_BASE . '/' . $napp . '/po/' . $lang . '.po';
+    $po = &new File_Gettext_PO();
+    $po->load($pofile);
+
+    // Set Scope
+    $lockscope = sprintf("babel-%s-%s", $app, $lang);
+    
+    // Initialize Horde_Lock class
+    $locks = &Horde_Lock::singleton('sql');
+    
+//    $curlocks = $locks->getLocks($lockscope);
+//    var_dump($curlocks);
+}
+
+//
+
+$f_cancel = Util::getFormData('cancel');
+$f_save = Util::getFormData('submit');
+
+if (($f_save || $f_cancel) && $cstring) {
+    if ($curlock = $locks->getLocks(md5($cstring), $lockscope)) {
+       foreach($curlock as $lid => $linfo) {
+           if ($linfo['lock_scope'] == md5($cstring)) {
+               $locks->clearLock($lid);
+           }
+       }
+    }
+}
+
+if ($f_save && $cstring) {
+    
+    $decstr = $po->encstr[$cstring];
+    $msgstr = Util::getFormData('msgstr');
+    $comments = trim($po->comments[$decstr]);
+
+    $phpformat = Util::getFormData('phpformat');
+    $fuzzy = Util::getFormData('fuzzy');
+    
+    $status = $po->status[$decstr];
+    foreach($status as $k => $v) {
+       if ($v == 'untranslated' && !empty($msgstr)) {
+           unset($status[$k]);
+       }
+       
+       if ($v == 'php-format' && !$phpformat) {
+           unset($status[$k]);
+       }
+       
+       if ($v == 'fuzzy' && !$fuzzy) {
+           unset($status[$k]);
+       }
+    }
+    
+    if (!in_array('php-format', $status) && $phpformat) {
+       $status[] = 'php-format';
+    }
+    
+    if (!in_array('fuzzy', $status) && $fuzzy) {
+       $status[] = 'fuzzy';
+    }
+    
+    $status = array_unique($status);
+    $po->status[$decstr] = $status;
+    
+    $status = '';
+    if (preg_match('/(#,.*)$/', $comments, $m)) {
+       $status = $m[1];
+    }
+    
+    if (count($po->status[$decstr])) {
+       $newstatus = "#, " . implode(', ', $po->status[$decstr]);
+    } else {
+       $newstatus = "";
+    }
+    
+    $newcomments = str_replace($status, $newstatus, $comments);
+    
+    $po->comments[$decstr] = $newcomments;
+    $po->strings[$decstr] = Translate_Display::convert_string($msgstr);
+    $po->save($pofile);
+}
+
+// 
+
+/* Set up the template fields. */
+$template->set('menu', Translation::getMenu('string'));
+$template->set('notify', Util::bufferOutput(array($notification, 'notify'), array('listeners' => 'status')));
+
+/* Create upload form */
+$form = &new Horde_Form($vars, _("View Translation"), $show);
+
+if (!$app) {
+    $form->setButtons(_("View"));
+    $form->addVariable(_("Module"), 'module', 'enum', true, false, null, array(Translation::listApps(), true));
+    $form->addVariable('', '', 'spacer', true);
+    
+    $renderer_params = array();
+    $renderer = &new Horde_Form_Renderer($renderer_params);
+    $renderer->setAttrColumnWidth('20%');
+    
+    $form->renderActive($renderer, $vars, Horde::selfURL(), 'post');
+} else {
+    
+    if (Translation::hasPermission('view', 'tabs', PERMS_EDIT)) {
+       $hmenu_desc = _("Edit Header");
+       $url = Horde::applicationUrl('edit.php');
+       $url = Util::addParameter($url, array('module' => $app,
+                                             'url'    => 'view'));
+       
+       $hmenu = Horde::link($url, $hmenu_desc, 'menuitem', null);
+       $hmenu .= Horde::img('edit.png', null, $hmenu_desc) . '&nbsp;' . $hmenu_desc . '</a>&nbsp;';
+    } else {
+       $hmenu = '';
+    }
+    
+    Translate_Display::header(_("Meta Informations"), $hmenu);
+    echo '<table border=0 width=100% style="border: solid 1px black" cellpadding=0 cellspacing=0>';
+    $i = 0;
+    foreach($po->meta as $k => $v) {
+       echo '<tr><td class="control" width=30%>';
+       echo $k;
+       echo '</td><td class="item' . ($i++ % 2) . '">';
+       echo htmlentities($v);
+       echo '</td><tr>';
+    }
+    echo '</table>';
+    Translate_Display::info();
+        
+    Translate_Display::header(_("Statistic"));
+    
+    $report = Translate::stats($app, $lang);
+    
+    echo '<table width="100%" align="center" border="0" cellspacing="0" cellpadding="0">';
+    echo '<tr class="control">';
+    echo '<td class="control" style="border-bottom: 1px solid #999;"><b>' . _("Language") . '</b></td>';
+    echo '<td width="5%"><b>' . _("Locale") . '</b></td>';
+    echo '<td width="15%"><b>' . _("Status") . '</b></td>';
+    echo '<td valign="bottom" style="width: 80px;"><b>' . _("Translated") . '</b></td>';
+    echo '<td valign="bottom" style="width: 80px;"><b>' . _("Fuzzy") . '</b></td>';
+    echo '<td valign="bottom" style="width: 80px;"><b>' . _("Untranslated") . '</b></td>';
+    echo '<td valign="bottom" style="width: 80px;"><b>' . _("Obsolete") . '</b></td>';
+    echo '</tr>';
+
+    echo "\n<tr class=\"item" . ($i++ % 2) . "\">";
+    echo "\n\t<td>" . $nls['languages'][$lang] . "</td>";
+    echo "\n\t<td>" . $lang . "</td>";
+    echo "\n\t<td>" . Translate_Display::create_bargraph(@$report[$lang][2], @$report[$lang][0]) . "</td>";
+    echo "\n\t<td>" . @$report[$lang][2] . "</td>";
+    echo "\n\t<td>" . @$report[$lang] [3] . "</td>";
+    echo "\n\t<td>" . @$report[$lang][4] . "</td>";
+    echo "\n\t<td>" . @$report[$lang][5] . "</td>";
+    echo "\t</tr>";
+    
+    echo '</table>';
+    Translate_Display::info();
+
+    $filter_html = '';
+    $filter_html .=  '<form action="' . Horde::applicationUrl('view.php') . '" method="post" name="edit" id="edit">';
+    $filter_html .= '<span class="smallheader">';
+    $filter_html .= Horde::img('edit.png') . '&nbsp;';
+    $filter_html .= '<b>' . _("Filter: ") . '</b>';
+    $filter_html .= '[&nbsp;';
+    if (!$filter) { 
+       $hmenu_desc = '<b>' . _("All") . '</b>';
+    } else {
+       $hmenu_desc = _("All");
+    }
+    $url = Horde::applicationUrl('view.php');
+    $url = Util::addParameter($url, array('module' => $app));
+    $filter_html .= Horde::link($url, _("Edit Mode"), 'menuitem', null). '&nbsp;' . $hmenu_desc . '</a>&nbsp;';
+    $filter_html .= '|&nbsp;';
+
+    if ($filter == 'translated') {
+       $hmenu_desc = '<b>' . _("Translated") . '</b>';
+    } else {
+       $hmenu_desc = _("Translated");
+    }
+    $url = Horde::applicationUrl('view.php');
+    $url = Util::addParameter($url, array('module' => $app, 'filter' => 'translated'));
+    $filter_html .= Horde::link($url, $hmenu_desc, 'menuitem', null). '&nbsp;' . $hmenu_desc . '</a>&nbsp;';
+    $filter_html .= '|&nbsp;';
+
+    
+    if ($filter == 'fuzzy') {
+       $hmenu_desc = '<b>' . _("Fuzzy") . '</b>';
+    } else {
+       $hmenu_desc = _("Fuzzy");
+    }
+    $url = Horde::applicationUrl('view.php');
+    $url = Util::addParameter($url, array('module' => $app, 'filter' => 'fuzzy'));
+    $filter_html .= Horde::link($url, $hmenu_desc, 'menuitem', null). '&nbsp;' . $hmenu_desc . '</a>&nbsp;';
+    $filter_html .= '|&nbsp;';
+
+    if ($filter == 'untranslated') {
+       $hmenu_desc = '<b>' . _("Untranslated") . '</b>';
+    } else {
+       $hmenu_desc = _("Untranslated");
+    }
+    $url = Horde::applicationUrl('view.php');
+    $url = Util::addParameter($url, array('module' => $app, 'filter' => 'untranslated'));
+    $filter_html .= Horde::link($url, $hmenu_desc, 'menuitem', null). '&nbsp;' . $hmenu_desc . '</a>&nbsp;';
+    $filter_html .= ']&nbsp;';
+
+    $filter_html .= '<input type="hidden" name="module" value="' . $app . '">';
+    $filter_html .= '<input type="hidden" name="page" value="' . $page . '">';
+    $filter_html .= '<input type="hidden" name="filter" value="' . $filter . '">';
+    $filter_html .= '<input type="text" name="search" value="' . $search . '">';
+    $filter_html .= '<input type="submit" name="filter_btn" value="' . _("Search") . '">';
+    $filter_html .= '</span>';
+    $filter_html .= '</form>';
+
+    $perpage = 100;
+    
+    foreach($po->strings as $msgid => $msgstr) {
+       if ($filter && !in_array($filter, $po->status[$msgid])) {
+           unset($po->strings[$msgid]);
+           unset($po->status[$msgid]);
+           unset($po->ref[$msgid]);
+       }
+       if ($search && !preg_match(';' . $search . ';i', $msgid)) {
+           unset($po->strings[$msgid]);
+           unset($po->status[$msgid]);
+           unset($po->ref[$msgid]);
+       }
+    }
+    
+    $numitem = count($po->strings);
+    // Set list min/max values
+    $min = $page * $perpage;
+    while ($min > $numitem) {
+       $page--;
+       $min = $page * $perpage;
+    }
+    $max = $min + $perpage;
+    
+    // Start start/end items (according to current page)
+    $start = ($page * $perpage) + 1;
+    $end = min($numitem, $start + $perpage - 1);
+    
+    $cntstr = 0;
+    
+    $pageinf = '&nbsp;<span class="smallheader">[' . sprintf(_("%s to %s of %s"), $start, $end, $numitem) . ']</span>';
+    Translate_Display::header(_("Translations") . $pageinf, $filter_html);
+    
+    foreach($po->strings as $msgid => $msgstr) {
+
+       $cntstr++;
+       
+       if ($start && $cntstr < $start) {
+           continue;
+       }
+       
+       if ($end && $cntstr > $end) {
+           break;
+       }
+       
+       if ($filter && !in_array($filter, $po->status[$msgid])) {
+           continue;
+       }
+       
+       $encstr = base64_encode($msgid);
+
+       $bgcolor = '1px #000000';
+       if (in_array('fuzzy', $po->status[$msgid])) {
+           $bgcolor = '3px #FFFF00';
+       }
+       
+       if (in_array('untranslated', $po->status[$msgid])) {
+           $bgcolor = '3px #FF0000';
+       }
+       
+       $locked = false;
+       if ($curlock = $locks->getLocks(md5($encstr), $lockscope)) {
+           foreach($curlock as $lid => $linfo) {
+               if ($linfo['lock_scope'] == md5($encstr)) {
+                   $bgcolor = '3px #FF00FF';
+                   $locked = $linfo['lock_owner'];
+               }
+           }
+       }
+       
+       if ($editmode && $cstring == $encstr) {
+           
+           // Lock the current item for 5 minutes
+           $locks->setLock(Auth::getAuth(), md5($encstr), $lockscope, 300);
+           
+           echo '<form action="' . Horde::applicationUrl('view.php') . "#" . md5($encstr) . '" method="post" name="edit" id="edit">';
+           echo '<input type="hidden" name="module" value="' . $app . '">';
+           echo '<input type="hidden" name="page" value="' . $page . '">';
+           echo '<input type="hidden" name="filter" value="' . $filter . '">';
+           echo '<input type="hidden" name="search" value="' . $search . '">';
+           echo '<input type="hidden" name="cstring" value="' . $encstr . '">';
+       }
+       
+       
+       ?>
+<a name="<?= md5($encstr) ?>">
+<table border=0 width=100% style="border: solid <?= $bgcolor ?>;">
+<tr>
+ <td valign=top  class="control" style="height: 18px; border-bottom: 1px solid #999;"><b>MSGID</b></td>
+ <td valign=top  class="control"  style="height: 18px; border-bottom: 1px solid #999;"><b>REFERENCES</b></td>
+ <td valign=top  class="control"  style="height: 18px; border-bottom: 1px solid #999;"><b>STATUS</b></td>
+</tr>
+<tr><td valign=top  class="item0">
+  <?= Translate_Display::display_string($msgid) ?><br />&nbsp;
+</td>
+<td valign=top  rowspan=3 width=30%>
+<table border=0 width=100% cellspacing=0 cellpadding=0>
+         
+<?php
+         $ref = array();
+       foreach($po->ref[$msgid] as $k => $v) {
+           if (preg_match('/(.*):(.*)/', $v, $m)) {
+               $sfile = $m[1];
+               $sline = $m[2];
+
+               if (Translation::hasPermission('viewsource', 'tabs', PERMS_EDIT)) {
+                   $surl = Horde::applicationUrl('viewsource.php');
+                   $surl = Util::addParameter($surl, array('module' => $app,
+                                                           'file'   => $sfile,
+                                                           'line'   => $sline));
+                   
+                   $onclick = "viewwindow=window.open('". $surl . "', 'viewsource', 'toolbar=no,location=no,status=yes,scrollbars=yes,resizable=yes,width=650,height=350,left=100,top=100'); if(window.focus) { viewwindow.focus()} ; return false;";
+                   
+                   $surl = Horde::link('#', $sline, null, null, $onclick);
+                   $surl .= $sline . '</a>';
+                   $surl = str_replace('&amp;', '&', $surl);
+               } else {
+                   $surl = $sline;
+               }
+               
+               $ref[$sfile][] = $surl;
+           }
+       }
+       
+       $i = 0;
+       foreach($ref as $k => $v) {
+           echo sprintf("<tr class=item%s><td>%s</td><td align=right>[ %s ]</td></tr>", ($i++ %2), $k, implode(' | ', $v));
+       }
+       ?>
+</table>       
+</td>
+<td valign=top  rowspan=3 width=10%>
+<?php
+         if ($editmode && $cstring == $encstr) {
+             if (in_array('php-format', $po->status[$msgid])) {
+                 echo '<input type="checkbox" checked name="phpformat">' . ' php-format<br>';
+             } else {
+                 echo '<input type="checkbox" name="phpformat">' . ' php-format<br>';
+             }
+             if (in_array('fuzzy', $po->status[$msgid])) {
+                 echo '<input type="checkbox" checked name="fuzzy">' . ' fuzzy<br>';
+             } else {
+                 echo '<input type="checkbox" name="fuzzy">' . ' fuzzy<br>';
+             }
+         } else {
+             echo implode('<br />', $po->status[$msgid]);
+         }
+       ?>      
+    
+</td>    
+</tr>  
+<tr>
+  <td valign=top class="control"  style="height: 18px; border-bottom: 1px solid #999;">
+  <table border="0" width="100%" cellspacing="0" cellpadding="0">
+  <tr>
+    <td><b>MSGSTR</b></td>
+    <td align="right">
+<?php
+         if ($locked) {
+             echo Horde::img('locked.png') . '&nbsp;' . sprintf(_("Locked by %s"), $locked);
+         } else {
+             if (Translation::hasPermission('view', 'tabs', PERMS_EDIT)) {
+                 if (!$editmode || $cstring != $encstr) {
+                     $surl = Horde::applicationUrl('view.php');
+                     $surl = Util::addParameter($surl, array('module' => $app, 'cstring' => $encstr, 'editmode' => 1, 'page' => $page, 'filter' => $filter, 'search' => $search));
+                     $surl .= "#" . md5($encstr);
+                     
+                     echo Horde::link($surl, _("Edit Translation")) . Horde::img('translation.png') . '&nbsp;' ._("Edit Translation") . "</a>";
+                 } elseif ($editmode && $cstring == $encstr) {
+                     echo '<input type="submit" class="button" name="submit" value="' . _("Save") . '">';
+                     echo '&nbsp;';
+                     echo '<input type="submit" class="button" name="cancel" value="' . _("Cancel") . '">';
+                 }
+             }
+         }
+?></td>
+  </tr>
+  </table>      
+</tr>
+<tr><td valign=top  class="item0">
+<?php
+         if ($editmode && $cstring == $encstr) {
+             echo '<textarea style="width:100%; height:100%" name="msgstr">' . Translate_Display::display_string($msgstr) . '</textarea><br />';
+         } else {
+             if (in_array('fuzzy', $po->status[$msgid]) && preg_match('/#-#-#-#-#/', $msgstr)) {
+                 preg_match_all('/(#-#-#-#-#\s+(.*?)\s+#-#-#-#-#(\n)*(.*))+/', Translate_Display::display_string($msgstr), $matches,  PREG_SET_ORDER);
+                 foreach($matches as $m) {
+                     echo sprintf("%s - %s<br />", $m[4], $m[2]);
+                 }
+             } else {
+                 echo Translate_Display::display_string($msgstr) . "<br />";
+             }
+         }
+?>&nbsp;
+</td>
+</tr>
+</table>
+<p />    
+<?php
+         flush();
+    
+       if ($editmode && $cstring == $encstr) {
+           echo '</form>';
+       }
+
+       //      print "STAT " . implode(', ', $po->status[$msgid]) . "<br>";
+    }
+}
+
+?>
+<!-- START PAGER -->
+<?php if (isset($numitem) && $numitem > $perpage): ?>
+<table width="100%" class="item box">
+<tr><td>
+<?php
+  $viewurl = Horde::applicationUrl('view.php');
+$viewurl = Util::addParameter($viewurl, array('editmode' => $editmode,
+                                             'module' => $app, 
+                                             'filter' => $filter,
+                                             'search' => $search));
+ $pager = &new Horde_UI_Pager('page', $vars, array('num' => $numitem, 'url' => $viewurl, 'page_count' => 10, 'perpage' => $perpage));
+ echo $pager->render($page, $numitem, $viewurl);
+?>
+</td></tr></table>
+<?php endif; ?>
+<!-- END PAGER -->
+<?php
+if ($app) {
+    Translation::RB_close();
+}
+
+require $registry->get('templates', 'horde') . '/common-footer.inc';
diff --git a/babel/viewsource.php b/babel/viewsource.php
new file mode 100644 (file)
index 0000000..c76eb7e
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+/**
+ * Copyright 2000-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author  Joel Vandal <joel@scopserv.com>
+ * @package Babel
+ */
+
+require_once dirname(__FILE__) . '/lib/base.php';
+
+$app   = Util::getFormData('module');
+$sfile = Util::getFormData('file');
+$sline = Util::getFormData('line');
+
+if ($app == 'horde') {
+    $srcfile = realpath(sprintf("%s/%s", HORDE_BASE, $sfile));
+} else {
+    $srcfile = realpath(sprintf("%s/%s/%s", HORDE_BASE, $app, $sfile));
+}
+
+if (empty($srcfile)) {
+    Horde::fatal(_("Missing filename!"), __FILE__, __LINE__, false);
+}
+
+$rpath = realpath(HORDE_BASE);
+if (!preg_match(";$rpath;", $srcfile)) {
+    Horde::fatal(sprintf(_("Access denied to %s"), $srcfile), __FILE__, __LINE__, false);
+}
+
+// Get File content
+$src = file_get_contents($srcfile);
+
+/* Render the page. */
+require BABEL_TEMPLATES . '/common-header.inc';
+
+Translate_Display::header(sprintf(_("View source: %s"), str_replace(realpath(HORDE_BASE) . '/', '', $srcfile)));
+
+printCode($src, $sline, 10);
+
+require $registry->get('templates', 'horde') . '/common-footer.inc';
+
+function printCode($code, $sline = 1, $sdiff = 10) {
+    if (!is_array($code)) $code = explode("\n", $code);
+    
+    $count_lines = count($code);
+    $r = '';
+    
+    $from = $sline - $sdiff;
+    $to   = $sline + $sdiff;
+    
+    foreach ($code as $line => $code_line) {
+       
+       if ($from && $line < $from) {
+           continue;
+       }
+       
+       if ($to && $line > $to) {
+           break;
+       }
+       
+       $r1 = ($line + 1);
+       
+       if (ereg("<\?(php)?[^[:graph:]]", $code_line)) {
+           $r2 = highlight_string($code_line, 1)."<br />";
+       } else {
+           $r2 = ereg_replace("(&lt;\?php&nbsp;)+", "", highlight_string("<?php ".$code_line, 1))."<br />";
+       }
+       
+       if ($r1 == $sline) {
+           $r .= sprintf('<tr><td align="right" class="control"><b>%s&nbsp;</b></td><td class="item0">%s</td></tr>', $r1, $r2);
+       } else {
+           $r .= sprintf('<tr><td align="right" class="control">%s&nbsp;</td><td>%s</td></tr>', $r1, $r2);
+       }
+    }
+    
+    $r = '<table width="100%" cellspacing=0>' . $r . '</table>';
+
+    echo "<div class=\"code\">".$r."</div>";
+}