<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[13268] sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events: Translate: Translation events sync from GitHub</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="http://meta.trac.wordpress.org/changeset/13268">13268</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"http://meta.trac.wordpress.org/changeset/13268","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>amieiro</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2024-03-04 11:41:16 +0000 (Mon, 04 Mar 2024)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Translate: Translation events sync from GitHub</pre>

<h3>Added Paths</h3>
<ul>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsLICENSE">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/LICENSE</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsREADMEmd">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/README.md</a></li>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/</li>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/css/</li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsassetscsstranslationeventscss">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/css/translation-events.css</a></li>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/js/</li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsassetsjstranslationeventsjs">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/js/translation-events.js</a></li>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/</li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsincludesactiveeventscachephp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/active-events-cache.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsincludeseventphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/event.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsincludesroutephp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/route.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsincludesstatscalculatorphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/stats-calculator.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsincludesstatslistenerphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/stats-listener.php</a></li>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/</li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateseventphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/event.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateseventsformphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-form.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateseventsheaderphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-header.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateseventslistphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-list.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateseventsmyeventsphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-my-events.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateshelperfunctionsphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/helper-functions.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventswporggptranslationeventsphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/wporg-gp-translation-events.php</a></li>
</ul>

<h3>Property Changed</h3>
<ul>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationevents">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<span class="cx" style="display: block; padding: 0 10px">Index: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events
</span><span class="cx" style="display: block; padding: 0 10px">===================================================================
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events 2024-03-04 04:54:42 UTC (rev 13267)
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events  2024-03-04 11:41:16 UTC (rev 13268)
</ins><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationevents"></a>
<div class="propset"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Property changes: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events</h4>
<pre class="diff"><span>
</span></pre></div>
<a id="svnignore"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:ignore</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+.github
+.wp-env.json
+bin
+composer.json
+composer.lock
+phpcs.xml
+phpunit.xml.dist
+tests
</ins><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsLICENSE"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/LICENSE</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/LICENSE                              (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/LICENSE        2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,339 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
</ins></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsREADMEmd"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/README.md</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/README.md                            (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/README.md      2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,42 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+# wporg-gp-translation-events
+
+## Development environment
+First follow [instructions to install `wp-env`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/#prerequisites).
+
+Then install dependencies:
+
+```shell
+composer install
+```
+
+Then you can run a local WordPress instance with the plugin installed:
+
+```shell
+composer dev:start
+```
+
+Once the environment is running, you must create the database tables needed by this plugin:
+
+```shell
+composer dev:db:schema
+```
+
+WordPress is now running at http://localhost:8888, user: `admin`, password: `password`.
+
+### Local environment
+
+If you are not using `wp-env`, you need to add the tables to the database of your local environment. To do this, you can run this command from the plugin folder:
+
+```shell
+wp db query < schema.sql
+```
+
+### Tests
+
+You can run tests in `wp-env` with the following command:
+
+> Note that `wp-env` must be running.
+
+```shell
+composer dev:test
+```
</ins></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsassetscsstranslationeventscss"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/css/translation-events.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/css/translation-events.css                            (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/css/translation-events.css      2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,235 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+.translation-event-form label {
+       display: inline-block;
+       width: 140px;
+       vertical-align: top;
+}
+.translation-event-form #event-title,
+.translation-event-form #event-description {
+       width: 30%;
+}
+.translation-event-form input[type="text"],
+.translation-event-form input[type="date"] {
+       width: 13%;
+}
+.translation-event-form div {
+       margin-top: 1em;
+}
+.translation-event-form #submit-event {
+       margin-left: 10%;
+       width: 30%;
+       margin-top: 1em;
+       display: block;
+}
+.event-page-title {
+       border-bottom: #d9d8d8 thin solid;
+       padding-bottom: .5em;
+}
+.event-details-page {
+       border-bottom: #cfd4d4 thin solid;
+       width: 60%;
+       margin: 0 auto;
+}
+.event-details-left {
+       width: 77%;
+       float: left;
+       border-right: #dadfdf thin solid;
+       margin-right: 1%;
+       padding-right: 1em;
+}
+.event-details-right {
+       width: 22%;
+       float: right;
+       padding-top: 1em;
+}
+.event-details-stats {
+       clear: both;
+       margin: 1em;
+}
+.event-details-stats table {
+       width: 100%;
+       table-layout: fixed;
+}
+.event-details-stats table th, .event-details-stats table td{
+       padding: 1em;
+       text-align: center;
+}
+.event-details-stats table tr {
+       border-bottom: thin solid #f0f0f0;
+}
+.event-details-stats table th {
+       background-color: #e9e9e9;
+}
+.event-details-stats-totals {
+       font-weight: bold;
+}
+.hide-event-url {
+       display: none;
+}
+.translation-event-form button.save-draft {
+       background: #fff;
+       color: var(--gp-color-btn-primary-bg);
+}
+#event-url span {
+       width: 10%;
+       display: inline-block;
+}
+span.event-list-date {
+       display: block;
+       color: #939393;
+       font-size: .8em;
+       font-weight: normal;
+}
+span.event-list-date.events-i-am-attending {
+       font-size: .8em;
+       font-weight: normal;
+}
+.event-list-item a{
+       font-weight: bold;
+       font-size: 1.2em;
+}
+li.event-list-item {
+       list-style-type: none;
+}
+li.event-list-item p {
+       margin-top: 0;
+       color: #5a5a5a;
+       font-size: .95em;
+}
+h2.event_page_title {
+       border-bottom: #e0e0e0 thin solid;
+       padding-bottom: 0.5em;
+       font-weight: bold;
+}
+.event-list-top-bar {
+       display: block;
+       width: 80%;
+       margin: 0 auto;
+       text-align: right;
+}
+.event-list-nav li {
+       display: inline;
+       margin: 0 0.1rem;
+}
+.event-list-nav li a {
+       text-decoration: none;
+       font-weight: bold;
+}
+.event-list-nav li:not(:first-child):not(:last-child):before {
+       content: " | ";
+}
+.event-left-col {
+       width: 75%;
+       float: left;
+       border-right: var(--gp-color-secondary-400) thin solid;
+}
+.event-right-col {
+       width: 25%;
+       float: left;
+       padding-left: 1em;
+}
+.event-attending-list {
+       list-style-type: none;
+       padding: 0;
+}
+.event-attending-list li {
+       margin-bottom: 1em;
+}
+.event-list-nav li a.button {
+       background-color: var(--gp-color-btn-primary-bg) !important;
+       color: #fff !important;
+}
+a.event-link-draft {
+       color:#80807f;
+}
+span.event-label-draft, span.event-details-join-expired, span.active-events-before-translation-table {
+       font-weight: 500;
+       color: var( --gp-color-bubble-inactive-project-text );
+       border: 1px solid var( --gp-color-bubble-inactive-project-text );
+       font-size: .7em;
+       margin-right: 0.3em;
+       width: 6em;
+       text-align: center;
+       padding: 0.2em 0.5em;
+       border-radius: 1em;
+       text-transform: capitalize;
+}
+span.active-events-before-translation-table a {
+       color: var( --gp-color-bubble-inactive-project-text );
+       text-decoration: none;
+}
+.active-events-before-translation-table {
+       width: 100%;
+       border: 1px solid var( --gp-color-border-default );
+       background: var( --gp-color-status-waiting-subtle );
+       margin: 1rem 0;
+       padding: 0.3rem 0.8rem;
+}
+.event-page-wrapper {
+       margin: 0 auto;
+       width: 80%;
+}
+.event-list {
+       margin: 0;
+       padding: 0;
+}
+.event-details-head {
+       border-bottom: var(--gp-color-secondary-400) thin solid;
+}
+input[type="submit"].attend-btn {
+       width: 100%;
+       text-align: center;
+       display: inline;
+       margin-top: 1em;
+       font-weight: bold;
+}
+input[type="submit"].attending-btn, a.button.is-primary.attend-btn {
+       width: 100%;
+       text-align: center;
+       display: block;
+       margin-top: 1em;
+}
+.event-details-date {
+       background: #f4f4f4;
+       color: #606161;
+       padding: .5em;
+       font-size: .9em;
+       line-height: 1.8em;
+       font-weight: 500;
+}
+.event-page-edit-link {
+       float: right;
+       text-decoration: none;
+}
+.event-stats-summary {
+       margin-top: 1em;
+}
+.event-stats-summary summary {
+       background: #f8f8f8;
+       padding: 0.4em;
+       cursor: pointer;
+       color: var(--gp-color-primary-400);
+       font-weight: 500;
+       font-size: .9em;
+}
+.event-stats-summary p.event-stats-text {
+       margin: 0;
+       background: #f8f8f8;
+       padding: 0.8em;
+       font-size: .9em;
+       border-top: thin solid #e0e0e0;
+       font-family: monospace;
+}
+time.event-utc-time {
+       display: block;
+       font-size: .95em;
+}
+.event-utc-time:first-of-type{
+       border-bottom: #cdcdcd thin solid;
+       padding-bottom: 0.5em;
+       margin-bottom: 0.5em;
+}
+span.event-details-date-label {
+       font-weight: bold;
+       color: #5a5a5a;
+       display: block;
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/css/translation-events.css
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsassetsjstranslationeventsjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/js/translation-events.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/js/translation-events.js                              (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/assets/js/translation-events.js        2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,170 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+(
+       function ( $, $gp ) {
+               jQuery( document ).ready(
+                       function ( $ ) {
+                               $gp.notices.init();
+                               const timezoneElement = $( '#event-timezone' );
+                               if ( timezoneElement.length && ! timezoneElement.val() ) {
+                                       selectUserTimezone();
+                               }
+                               validateEventDates();
+                               convertToUserLocalTime();
+
+                               $( '.submit-event' ).on(
+                                       'click',
+                                       function ( e ) {
+                                               e.preventDefault();
+                                               let eventStatus = $( this ).data( 'event-status' );
+                                               let isDraft     = $( 'button.save-draft[data-event-status="draft"]:visible' ).length > 0;
+                                               handleSubmit( eventStatus, isDraft );
+                                       }
+                               );
+
+                               $( '.delete-event' ).on(
+                                       'click',
+                                       function ( e ) {
+                                               e.preventDefault();
+                                               handleDelete()
+                                       }
+                               );
+                       }
+               );
+
+               /**
+                * Handles the form submission
+                *
+                * @param eventStatus The new status of the event
+                * @param isDraft         Whether the current event status is a draft or not
+                */
+               function handleSubmit( eventStatus, isDraft ) {
+                       if ( '' === $( '#event-start' ).val() ) {
+                               $gp.notices.error( 'Event start date and time must be set.' );
+                               return;
+                       }
+                       if ( '' === $( '#event-end' ).val() ) {
+                               $gp.notices.error( 'Event end date and time must be set.' );
+                               return;
+                       }
+                       if ( $( '#event-end' ).val() <= $( '#event-start' ).val() ) {
+                               $gp.notices.error( 'Event end date and time must be later than event start date and time.' );
+                               return;
+                       }
+                       if ( eventStatus === 'publish' && isDraft ) {
+                               const submitPrompt = 'Are you sure you want to publish this event?';
+                               if ( ! confirm( submitPrompt ) ) {
+                                       return;
+                               }
+                       }
+                       $( '#event-form-action' ).val( eventStatus );
+                       const $form        = $( '.translation-event-form' );
+                       const $is_creation = $( '#form-name' ).val() === 'create_event';
+
+                       $.ajax(
+                               {
+                                       type: 'POST',
+                                       url: $translation_event.url,
+                                       data:$form.serialize(),
+                                       success: function ( response ) {
+                                               if ( response.data.eventId ) {
+                                                       history.replaceState( '', '', response.data.eventEditUrl );
+                                                       $( '#form-name' ).val( 'edit_event' );
+                                                       $( '.event-page-title' ).text( 'Edit Event' );
+                                                       $( '#event-id' ).val( response.data.eventId );
+                                                       if ( eventStatus === 'publish' ) {
+                                                               $( 'button[data-event-status="draft"]' ).hide();
+                                                               $( '#published-update-text' ).show();
+                                                               $( 'button[data-event-status="publish"]' ).text( 'Update Event' );
+                                                       }
+                                                       if ( eventStatus === 'draft' ) {
+                                                               $( 'button[data-event-status="draft"]' ).text( 'Update Draft' );
+                                                       }
+                                                       $( '#event-url' ).removeClass( 'hide-event-url' ).find( 'a' ).attr( 'href', response.data.eventUrl ).text( response.data.eventUrl );
+                                                       if ( $is_creation ) {
+                                                               $( '#delete-button' ).toggle();
+                                                       }
+                                                       $gp.notices.success( response.data.message );
+                                               }
+                                       },
+                                       error: function ( xhr, msg ) {
+                                               /* translators: %s: Error message. */
+                                               msg = xhr.responseJSON.data ? wp.i18n.sprintf( wp.i18n.__( 'Error: %s', 'gp-translation-events' ), xhr.responseJSON.data ) : wp.i18n.__( 'Error saving the event!', 'gp-translation-events' );
+                                               $gp.notices.error( msg );
+                                       },
+                               }
+                       );
+               }
+
+               function handleDelete() {
+                       if ( ! confirm( 'Are you sure you want to delete this event?' ) ) {
+                               return;
+                       }
+                       const $form = $( '.translation-event-form' );
+                       $( '#form-name' ).val( 'delete_event' );
+                       $( '#event-form-action' ).val( 'delete' );
+                       $.ajax(
+                               {
+                                       type: 'POST',
+                                       url: $translation_event.url,
+                                       data:$form.serialize(),
+                                       success: function ( response ) {
+                                               window.location = response.data.eventDeleteUrl;
+                                       },
+                                       error: function ( error ) {
+                                               $gp.notices.error( response.data.message );
+                                       },
+                               }
+                       );
+               }
+
+               function validateEventDates() {
+                       const startDateTimeInput = $( '#event-start' );
+                       const endDateTimeInput   = $( '#event-end' );
+                       if ( ! startDateTimeInput.length || ! endDateTimeInput.length ) {
+                               return;
+                       }
+
+                       startDateTimeInput.add( endDateTimeInput ).on(
+                               'input',
+                               function () {
+                                       endDateTimeInput.prop( 'min', startDateTimeInput.val() );
+                                       if (endDateTimeInput.val() < startDateTimeInput.val()) {
+                                               endDateTimeInput.val( startDateTimeInput.val() );
+                                       }
+                               }
+                       );
+               }
+               function selectUserTimezone() {
+                       const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+                       // phpcs:disable WordPress.WhiteSpace.OperatorSpacing.NoSpaceBefore
+                       // phpcs:disable WordPress.WhiteSpace.OperatorSpacing.NoSpaceAfter
+                       document.querySelector( `#event-timezone option[value="${timezone}"]` ).selected = true
+                       // phpcs:enable
+               }
+
+               function convertToUserLocalTime() {
+                       const timeElements = document.querySelectorAll( 'time.event-utc-time' );
+                       if ( timeElements.length === 0 ) {
+                               return;
+                       }
+                       timeElements.forEach(
+                               function ( timeEl ) {
+                                       const eventDateObj         = new Date( timeEl.getAttribute( 'datetime' ) );
+                                       const userTimezoneOffset   = new Date().getTimezoneOffset();
+                                       const userTimezoneOffsetMs = userTimezoneOffset * 60 * 1000;
+                                       const userLocalDateTime    = new Date( eventDateObj.getTime() - userTimezoneOffsetMs );
+
+                                       const options      = {
+                                               weekday: 'short',
+                                               year: 'numeric',
+                                               month: 'short',
+                                               day: 'numeric',
+                                               hour: 'numeric',
+                                               minute: 'numeric',
+                                               timeZoneName: 'short'
+                                       };
+                                       timeEl.textContent = userLocalDateTime.toLocaleTimeString( navigator.language, options );
+                               }
+                       );
+               }
+       }( jQuery, $gp )
+);
</ins></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsincludesactiveeventscachephp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/active-events-cache.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/active-events-cache.php                             (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/active-events-cache.php       2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,53 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace Wporg\TranslationEvents;
+
+use Exception;
+
+class Active_Events_Cache {
+       public const CACHE_DURATION = 60 * 60 * 24; // 24 hours.
+       private const KEY           = 'translation-events-active-events';
+
+       /**
+        * Cache active events.
+        *
+        * @param Event[] $events Events to cache.
+        *
+        * @throws Exception When it fails to cache events.
+        */
+       public function cache( array $events ): void {
+               if ( ! wp_cache_set( self::KEY, $events, '', self::CACHE_DURATION ) ) {
+                       throw new Exception( 'Failed to cache active events' );
+               }
+       }
+
+       /**
+        * Returns the cached events, or null if nothing is cached.
+        *
+        * @return Event[]|null
+        * @throws Exception When it fails to retrieve cached events.
+        */
+       public function get(): ?array {
+               $result = wp_cache_get( self::KEY, '', false, $found );
+               if ( ! $found ) {
+                       return null;
+               }
+
+               if ( ! is_array( $result ) ) {
+                       throw new Exception( 'Cached events is not an array, something is wrong' );
+               }
+
+               return $result;
+       }
+
+       /**
+        * Invalidates the active events cache.
+        *
+        * @throws Exception When it fails to invalidate the cache.
+        */
+       public static function invalidate(): void {
+               if ( ! wp_cache_delete( self::KEY ) ) {
+                       throw new Exception( 'Failed to invalidate cached events' );
+               }
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/active-events-cache.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsincludeseventphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/event.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/event.php                           (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/event.php     2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,80 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace Wporg\TranslationEvents;
+
+use DateTimeImmutable;
+use DateTimeZone;
+use Exception;
+
+class Event {
+       private int $id;
+       private DateTimeImmutable $start;
+       private DateTimeImmutable $end;
+       private DateTimeZone $timezone;
+
+       /**
+        * Make an Event from post meta.
+        *
+        * @throws Exception When dates are invalid.
+        */
+       public static function from_post_meta( int $id, array $meta ): Event {
+               if ( ! isset( $meta['_event_start'][0] ) || ! isset( $meta['_event_end'][0] ) || ! isset( $meta['_event_timezone'][0] ) ) {
+                       throw new Exception( 'Invalid event meta' );
+               }
+
+               return new Event(
+                       $id,
+                       DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $meta['_event_start'][0], new DateTimeZone( 'UTC' ) ),
+                       DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $meta['_event_end'][0], new DateTimeZone( 'UTC' ) ),
+                       new DateTimeZone( $meta['_event_timezone'][0] ),
+               );
+       }
+
+       private function __construct( int $id, DateTimeImmutable $start, DateTimeImmutable $end, DateTimeZone $timezone ) {
+               $this->id       = $id;
+               $this->start    = $start;
+               $this->end      = $end;
+               $this->timezone = $timezone;
+       }
+
+       public function id(): int {
+               return $this->id;
+       }
+
+       public function start(): DateTimeImmutable {
+               return $this->start;
+       }
+
+       public function end(): DateTimeImmutable {
+               return $this->end;
+       }
+
+       public function timezone(): DateTimeZone {
+               return $this->timezone;
+       }
+
+       /**
+        * Generate text for the end date.
+        *
+        * @param string $event_end The end date.
+        *
+        * @return string The end date text.
+        */
+       public static function get_end_date_text( string $event_end ): string {
+               $end_date_time     = new DateTimeImmutable( $event_end );
+               $current_date_time = new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) );
+
+               $interval       = $end_date_time->diff( $current_date_time );
+               $hours_left     = ( $interval->d * 24 ) + $interval->h;
+               $hours_in_a_day = 24;
+
+               if ( 0 === $hours_left ) {
+                       /* translators: %s: Number of minutes left. */
+                       return sprintf( _n( 'ends in %s minute', 'ends in %s minutes', $interval->i ), $interval->i );
+               } elseif ( $hours_left <= $hours_in_a_day ) {
+                       /* translators: %s: Number of hours left. */
+                       return sprintf( _n( 'ends in %s hour', 'ends in %s hours', $hours_left ), $hours_left );
+               }
+               return sprintf( 'until %s', $end_date_time->format( 'M j, Y' ) );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/event.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsincludesroutephp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/route.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/route.php                           (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/route.php     2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,404 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace Wporg\TranslationEvents;
+
+use DateTime;
+use DateTimeZone;
+use Exception;
+use GP_Route;
+use WP_Query;
+use GP;
+
+class Route extends GP_Route {
+       public const USER_META_KEY_ATTENDING = 'translation-events-attending';
+
+       public function __construct() {
+               parent::__construct();
+               $this->template_path = __DIR__ . '/../templates/';
+       }
+
+       /**
+        * Loads the 'events_list' template.
+        *
+        * @return void
+        */
+       public function events_list() {
+               $current_datetime_utc = null;
+               try {
+                       $current_datetime_utc = ( new DateTime( 'now', new DateTimeZone( 'UTC' ) ) )->format( 'Y-m-d H:i:s' );
+               } catch ( Exception $e ) {
+                       // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+                       error_log( $e );
+                       $this->die_with_error( esc_html__( 'Something is wrong.', 'gp-translation-events' ) );
+               }
+
+               $_current_events_paged        = 1;
+               $_upcoming_events_paged       = 1;
+               $_past_events_paged           = 1;
+               $_user_attending_events_paged = 1;
+
+               // phpcs:disable WordPress.Security.NonceVerification.Recommended
+               if ( isset( $_GET['current_events_paged'] ) ) {
+                       $value = sanitize_text_field( wp_unslash( $_GET['current_events_paged'] ) );
+                       if ( is_numeric( $value ) ) {
+                               $_current_events_paged = (int) $value;
+                       }
+               }
+               if ( isset( $_GET['upcoming_events_paged'] ) ) {
+                       $value = sanitize_text_field( wp_unslash( $_GET['upcoming_events_paged'] ) );
+                       if ( is_numeric( $value ) ) {
+                               $_upcoming_events_paged = (int) $value;
+                       }
+               }
+               if ( isset( $_GET['past_events_paged'] ) ) {
+                       $value = sanitize_text_field( wp_unslash( $_GET['past_events_paged'] ) );
+                       if ( is_numeric( $value ) ) {
+                               $_past_events_paged = (int) $value;
+                       }
+               }
+               if ( isset( $_GET['user_attending_events_paged'] ) ) {
+                       $value = sanitize_text_field( wp_unslash( $_GET['user_attending_events_paged'] ) );
+                       if ( is_numeric( $value ) ) {
+                               $_user_attending_events_paged = (int) $value;
+                       }
+               }
+               // phpcs:enable
+
+               $current_events_args  = array(
+                       'post_type'      => Translation_Events::CPT,
+                       'posts_per_page' => 10,
+                       'paged'          => $_current_events_paged,
+                       'post_status'    => 'publish',
+                       // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+                       'meta_query'     => array(
+                               array(
+                                       'key'     => '_event_start',
+                                       'value'   => $current_datetime_utc,
+                                       'compare' => '<=',
+                                       'type'    => 'DATETIME',
+                               ),
+                               array(
+                                       'key'     => '_event_end',
+                                       'value'   => $current_datetime_utc,
+                                       'compare' => '>=',
+                                       'type'    => 'DATETIME',
+                               ),
+                       ),
+                       'orderby'        => 'meta_value',
+                       'order'          => 'ASC',
+               );
+               $current_events_query = new WP_Query( $current_events_args );
+
+               $upcoming_events_args  = array(
+                       'post_type'      => Translation_Events::CPT,
+                       'posts_per_page' => 10,
+                       'paged'          => $_upcoming_events_paged,
+                       'post_status'    => 'publish',
+                       // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+                       'meta_query'     => array(
+                               array(
+                                       'key'     => '_event_start',
+                                       'value'   => $current_datetime_utc,
+                                       'compare' => '>=',
+                                       'type'    => 'DATETIME',
+                               ),
+                       ),
+                       'orderby'        => 'meta_value',
+                       'order'          => 'ASC',
+               );
+               $upcoming_events_query = new WP_Query( $upcoming_events_args );
+
+               $past_events_args  = array(
+                       'post_type'      => Translation_Events::CPT,
+                       'posts_per_page' => 10,
+                       'paged'          => $_past_events_paged,
+                       'post_status'    => 'publish',
+                       // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+                       'meta_query'     => array(
+                               array(
+                                       'key'     => '_event_end',
+                                       'value'   => $current_datetime_utc,
+                                       'compare' => '<',
+                                       'type'    => 'DATETIME',
+                               ),
+                       ),
+                       'orderby'        => 'meta_value',
+                       'order'          => 'ASC',
+               );
+               $past_events_query = new WP_Query( $past_events_args );
+
+               $user_attending_events      = get_user_meta( get_current_user_id(), self::USER_META_KEY_ATTENDING, true ) ?: array( 0 );
+               $user_attending_events_args = array(
+                       'post_type'      => Translation_Events::CPT,
+                       'post__in'       => array_keys( $user_attending_events ),
+                       'posts_per_page' => 10,
+                       'paged'          => $_user_attending_events_paged,
+                       'post_status'    => 'publish',
+                       // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+                       'meta_query'     => array(
+                               array(
+                                       'key'     => '_event_end',
+                                       'value'   => $current_datetime_utc,
+                                       'compare' => '>',
+                                       'type'    => 'DATETIME',
+                               ),
+                       ),
+                       // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+                       'meta_key'       => '_event_start',
+                       'orderby'        => 'meta_value',
+                       'order'          => 'ASC',
+               );
+               $user_attending_events_query = new WP_Query( $user_attending_events_args );
+
+               $this->tmpl( 'events-list', get_defined_vars() );
+       }
+
+       /**
+        * Loads the 'events_create' template.
+        *
+        * @return void
+        */
+       public function events_create() {
+               global $wp;
+               if ( ! is_user_logged_in() ) {
+                       wp_safe_redirect( wp_login_url( home_url( $wp->request ) ) );
+                       exit;
+               }
+               $event_form_title         = 'Create Event';
+               $event_form_name          = 'create_event';
+               $css_show_url             = 'hide-event-url';
+               $event_id                 = null;
+               $event_title              = '';
+               $event_description        = '';
+               $event_timezone           = '';
+               $event_start              = '';
+               $event_end                = '';
+               $event_url                = '';
+               $create_delete_button     = true;
+               $visibility_delete_button = 'none';
+
+               $this->tmpl( 'events-form', get_defined_vars() );
+       }
+
+       /**
+        * Loads the 'events_edit' template.
+        *
+        * @param int $event_id The event ID.
+        *
+        * @return void
+        */
+       public function events_edit( int $event_id ) {
+               global $wp;
+               if ( ! is_user_logged_in() ) {
+                       wp_safe_redirect( wp_login_url( home_url( $wp->request ) ) );
+                       exit;
+               }
+               $event = get_post( $event_id );
+               if ( ! $event || Translation_Events::CPT !== $event->post_type || ! ( current_user_can( 'edit_post', $event->ID ) || intval( $event->post_author ) === get_current_user_id() ) ) {
+                       $this->die_with_error( esc_html__( 'Event does not exist, or you do not have permission to edit it.', 'gp-translation-events' ), 403 );
+               }
+               if ( 'trash' === $event->post_status ) {
+                       $this->die_with_error( esc_html__( 'You cannot edit a trashed event', 'gp-translation-events' ), 403 );
+               }
+
+               include ABSPATH . 'wp-admin/includes/post.php';
+               $event_form_title              = 'Edit Event';
+               $event_form_name               = 'edit_event';
+               $css_show_url                  = '';
+               $event_title                   = $event->post_title;
+               $event_description             = $event->post_content;
+               $event_status                  = $event->post_status;
+               list( $permalink, $post_name ) = get_sample_permalink( $event_id );
+               $permalink                     = str_replace( '%pagename%', $post_name, $permalink );
+               $event_url                     = get_site_url() . gp_url( wp_make_link_relative( $permalink ) );
+               $event_timezone                = get_post_meta( $event_id, '_event_timezone', true ) ?: '';
+               $create_delete_button          = false;
+               $visibility_delete_button      = 'inline-flex';
+
+               $stats_calculator = new Stats_Calculator();
+               if ( ! $stats_calculator->event_has_stats( $event ) ) {
+                       $current_user = wp_get_current_user();
+                       if ( $current_user->ID === $event->post_author || current_user_can( 'manage_options' ) ) {
+                               $create_delete_button = true;
+                       }
+               }
+
+               try {
+                       $event_start = self::convertToTimezone( get_post_meta( $event_id, '_event_start', true ), $event_timezone );
+                       $event_end   = self::convertToTimezone( get_post_meta( $event_id, '_event_end', true ), $event_timezone );
+               } catch ( Exception $e ) {
+                       // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+                       error_log( $e );
+                       $this->die_with_error( esc_html__( 'Something is wrong.', 'gp-translation-events' ) );
+               }
+
+               $this->tmpl( 'events-form', get_defined_vars() );
+       }
+
+       /**
+        * Loads the 'event' template.
+        *
+        * @param string $event_slug The event slug.
+        *
+        * @return void
+        */
+       public function events_details( string $event_slug ) {
+               $user  = wp_get_current_user();
+               $event = get_page_by_path( $event_slug, OBJECT, Translation_Events::CPT );
+               if ( ! $event ) {
+                       $this->die_with_404();
+               }
+               /**
+                * Filter the ability to create, edit, or delete an event.
+                *
+                * @param bool $can_crud_event Whether the user can create, edit, or delete an event.
+                */
+               $can_crud_event = apply_filters( 'gp_translation_events_can_crud_event', GP::$permission->current_user_can( 'admin' ) );
+               if ( 'publish' !== $event->post_status && ! $can_crud_event ) {
+                       $this->die_with_error( esc_html__( 'You are not authorized to view this page.', 'gp-translation-events' ), 403 );
+               }
+
+               $event_id            = $event->ID;
+               $event_title         = $event->post_title;
+               $event_description   = $event->post_content;
+               $event_start         = get_post_meta( $event->ID, '_event_start', true ) ?: '';
+               $event_end           = get_post_meta( $event->ID, '_event_end', true ) ?: '';
+               $attending_event_ids = get_user_meta( $user->ID, self::USER_META_KEY_ATTENDING, true ) ?: array();
+               $user_is_attending   = isset( $attending_event_ids[ $event_id ] );
+
+               $stats_calculator = new Stats_Calculator();
+               try {
+                       $event_stats = $stats_calculator->for_event( $event );
+               } catch ( Exception $e ) {
+                       // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+                       error_log( $e );
+                       $this->die_with_error( esc_html__( 'Failed to calculate event stats', 'gp-translation-events' ) );
+               }
+
+               $this->tmpl( 'event', get_defined_vars() );
+       }
+
+       /**
+        * Toggle whether the current user is attending an event.
+        * If the user is not currently marked as attending, they will be marked as attending.
+        * If the user is currently marked as attending, they will be marked as not attending.
+        */
+       public function events_attend( int $event_id ) {
+               $user = wp_get_current_user();
+               if ( ! $user ) {
+                       $this->die_with_error( esc_html__( 'Only logged-in users can attend events', 'gp-translation-events' ), 403 );
+               }
+
+               $event = get_post( $event_id );
+
+               if ( ! $event ) {
+                       $this->die_with_404();
+               }
+
+               $event_ids = get_user_meta( $user->ID, self::USER_META_KEY_ATTENDING, true ) ?? array();
+               if ( ! $event_ids ) {
+                       $event_ids = array();
+               }
+
+               if ( ! isset( $event_ids[ $event_id ] ) ) {
+                       // Not yet attending, mark as attending.
+                       $event_ids[ $event_id ] = true;
+               } else {
+                       // Currently attending, mark as not attending.
+                       unset( $event_ids[ $event_id ] );
+               }
+
+               update_user_meta( $user->ID, self::USER_META_KEY_ATTENDING, $event_ids );
+
+               wp_safe_redirect( gp_url( "/events/$event->post_name" ) );
+               exit;
+       }
+
+       /**
+        * Loads the 'events_user_created' template.
+        *
+        * @return void
+        */
+       public function my_events() {
+               global $wp;
+               if ( ! is_user_logged_in() ) {
+                       wp_safe_redirect( wp_login_url( home_url( $wp->request ) ) );
+                       exit;
+               }
+               include ABSPATH . 'wp-admin/includes/post.php';
+
+               $_events_i_created_paged  = 1;
+               $_events_i_attended_paged = 1;
+
+               // phpcs:disable WordPress.Security.NonceVerification.Recommended
+               if ( isset( $_GET['events_i_created_paged'] ) ) {
+                       $value = sanitize_text_field( wp_unslash( $_GET['events_i_created_paged'] ) );
+                       if ( is_numeric( $value ) ) {
+                               $_events_i_created_paged = (int) $value;
+                       }
+               }
+               if ( isset( $_GET['events_i_attended_paged'] ) ) {
+                       $value = sanitize_text_field( wp_unslash( $_GET['events_i_attended_paged'] ) );
+                       if ( is_numeric( $value ) ) {
+                               $_events_i_attended_paged = (int) $value;
+                       }
+               }
+               // phpcs:enable
+
+               $user_id              = get_current_user_id();
+               $events               = get_user_meta( $user_id, self::USER_META_KEY_ATTENDING, true ) ?: array();
+               $events               = array_keys( $events );
+               $current_datetime_utc = ( new DateTime( 'now', new DateTimeZone( 'UTC' ) ) )->format( 'Y-m-d H:i:s' );
+               $args                 = array(
+                       'post_type'              => Translation_Events::CPT,
+                       'posts_per_page'         => 10,
+                       'events_i_created_paged' => $_events_i_created_paged,
+                       'paged'                  => $_events_i_created_paged,
+                       'post_status'            => array( 'publish', 'draft' ),
+                       'author'                 => $user_id,
+                       // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+                       'meta_key'               => '_event_start',
+                       'orderby'                => 'meta_value',
+                       'order'                  => 'DESC',
+               );
+               $events_i_created_query = new WP_Query( $args );
+
+               $args = array(
+                       'post_type'               => Translation_Events::CPT,
+                       'posts_per_page'          => 10,
+                       'events_i_attended_paged' => $_events_i_attended_paged,
+                       'paged'                   => $_events_i_attended_paged,
+                       'post_status'             => 'publish',
+                       'post__in'                => $events,
+                       // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+                       'meta_query'              => array(
+                               array(
+                                       'key'     => '_event_end',
+                                       'value'   => $current_datetime_utc,
+                                       'compare' => '<',
+                                       'type'    => 'DATETIME',
+                               ),
+                       ),
+                       // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+                       'meta_key'                => '_event_end',
+                       'orderby'                 => 'meta_value',
+                       'order'                   => 'DESC',
+               );
+               $events_i_attended_query = new WP_Query( $args );
+
+               $this->tmpl( 'events-my-events', get_defined_vars() );
+       }
+
+       /**
+        * Convert date time stored in UTC to a date time in a time zone.
+        *
+        * @param string $date_time The date time in UTC.
+        * @param string $time_zone The time zone.
+        *
+        * @return string The date time in the time zone.
+        * @throws Exception When date is invalid.
+        */
+       public static function convertToTimezone( string $date_time, string $time_zone ): string {
+               return ( new DateTime( $date_time, new DateTimeZone( 'UTC' ) ) )->setTimezone( new DateTimeZone( $time_zone ) )->format( 'Y-m-d H:i:s' );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/route.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsincludesstatscalculatorphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/stats-calculator.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/stats-calculator.php                                (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/stats-calculator.php  2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,134 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace Wporg\TranslationEvents;
+
+use Exception;
+use WP_Post;
+
+class Stats_Row {
+       public int $created;
+       public int $reviewed;
+       public int $users;
+
+       public function __construct( $created, $reviewed, $users ) {
+               $this->created  = $created;
+               $this->reviewed = $reviewed;
+               $this->users    = $users;
+       }
+}
+
+class Event_Stats {
+       /**
+        * Associative array of rows, with the locale as key.
+        *
+        * @var Stats_Row[]
+        */
+       private array $rows = array();
+
+       private Stats_Row $totals;
+
+       /**
+        * Add a stats row.
+        *
+        * @throws Exception When incorrect locale is passed.
+        */
+       public function add_row( string $locale, Stats_Row $row ) {
+               if ( ! $locale ) {
+                       throw new Exception( 'locale must not be empty' );
+               }
+               $this->rows[ $locale ] = $row;
+       }
+
+       public function set_totals( Stats_Row $totals ) {
+               $this->totals = $totals;
+       }
+
+       /**
+        * Get an associative array of rows, with the locale as key.
+        *
+        * @return Stats_Row[]
+        */
+       public function rows(): array {
+               return $this->rows;
+       }
+
+       public function totals(): Stats_Row {
+               return $this->totals;
+       }
+}
+
+class Stats_Calculator {
+       /**
+        * Get stats for an event.
+        *
+        * @throws Exception When stats calculation failed.
+        */
+       public function for_event( WP_Post $event ): Event_Stats {
+               $stats = new Event_Stats();
+               global $wpdb;
+
+               // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
+               // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
+               // phpcs thinks we're doing a schema change but we aren't.
+               // phpcs:disable WordPress.DB.DirectDatabaseQuery.SchemaChange
+               $rows = $wpdb->get_results(
+                       $wpdb->prepare(
+                               "
+                               select locale,
+                                          sum(action = 'create') as created,
+                                          count(*) as total,
+                                          count(distinct user_id) as users
+                               from {$wpdb->base_prefix}event_actions
+                               where event_id = %d
+                               group by locale with rollup
+                       ",
+                               array(
+                                       $event->ID,
+                               )
+                       )
+               );
+               // phpcs:enable
+
+               foreach ( $rows as $index => $row ) {
+                       $is_totals = null === $row->locale;
+                       if ( $is_totals && array_key_last( $rows ) !== $index ) {
+                               // If this is not the last row, something is wrong in the data in the database table
+                               // or there's a bug in the query above.
+                               throw new Exception(
+                                       'Only the last row should have no locale but we found a non-last row with no locale.'
+                               );
+                       }
+
+                       $stats_row = new Stats_Row(
+                               $row->created,
+                               $row->total - $row->created,
+                               $row->users,
+                       );
+
+                       if ( ! $is_totals ) {
+                               $stats->add_row( $row->locale, $stats_row );
+                       } else {
+                               $stats->set_totals( $stats_row );
+                       }
+               }
+
+               return $stats;
+       }
+
+       /**
+        * Check if an event has stats.
+        *
+        * @param WP_Post $event The event to check.
+        *
+        * @return bool True if the event has stats, false otherwise.
+        */
+       public function event_has_stats( WP_Post $event ): bool {
+               try {
+                       $stats = $this->for_event( $event );
+               } catch ( Exception $e ) {
+                       return false;
+               }
+
+               return ! empty( $stats->rows() );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/stats-calculator.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventsincludesstatslistenerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/stats-listener.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/stats-listener.php                          (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/stats-listener.php    2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,179 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace Wporg\TranslationEvents;
+
+use DateTimeImmutable;
+use DateTimeZone;
+use Exception;
+use GP_Translation;
+use GP_Translation_Set;
+
+class Stats_Listener {
+       const ACTION_CREATE          = 'create';
+       const ACTION_APPROVE         = 'approve';
+       const ACTION_REJECT          = 'reject';
+       const ACTION_REQUEST_CHANGES = 'request_changes';
+
+       private Active_Events_Cache $active_events_cache;
+
+       public function __construct( Active_Events_Cache $active_events_cache ) {
+               $this->active_events_cache = $active_events_cache;
+       }
+
+       public function start(): void {
+               add_action(
+                       'gp_translation_created',
+                       function ( $translation ) {
+                               $happened_at = DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $translation->date_added, new DateTimeZone( 'UTC' ) );
+                               $this->handle_action( $translation, $translation->user_id, self::ACTION_CREATE, $happened_at );
+                       },
+               );
+
+               add_action(
+                       'gp_translation_saved',
+                       function ( $translation, $translation_before ) {
+                               $user_id     = $translation->user_id_last_modified;
+                               $status      = $translation->status;
+                               $happened_at = DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $translation->date_modified, new DateTimeZone( 'UTC' ) );
+
+                               if ( $translation_before->status === $status ) {
+                                       // Translation hasn't changed status, so there's nothing for us to track.
+                                       return;
+                               }
+
+                               $action = null;
+                               switch ( $status ) {
+                                       case 'current':
+                                               $action = self::ACTION_APPROVE;
+                                               break;
+                                       case 'rejected':
+                                               $action = self::ACTION_REJECT;
+                                               break;
+                                       case 'changesrequested':
+                                               $action = self::ACTION_REQUEST_CHANGES;
+                                               break;
+                               }
+
+                               if ( $action ) {
+                                       $this->handle_action( $translation, $user_id, $action, $happened_at );
+                               }
+                       },
+                       10,
+                       2,
+               );
+       }
+
+       private function handle_action( GP_Translation $translation, int $user_id, string $action, DateTimeImmutable $happened_at ): void {
+               try {
+                       // Get events that are active when the action happened, for which the user is registered for.
+                       $active_events = $this->get_active_events( $happened_at );
+                       $events        = $this->select_events_user_is_registered_for( $active_events, $user_id );
+
+                       // phpcs:ignore Generic.Commenting.DocComment.MissingShort
+                       /** @var GP_Translation_Set $translation_set Translation set */
+                       $translation_set = ( new GP_Translation_Set() )->find_one( array( 'id' => $translation->translation_set_id ) );
+                       global $wpdb;
+
+                       foreach ( $events as $event ) {
+                               // A given user can only do one action on a specific translation.
+                               // So we insert ignore, which will keep only the first action.
+                               // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
+                               // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
+                               $wpdb->query(
+                                       $wpdb->prepare(
+                                               "insert ignore into {$wpdb->base_prefix}event_actions (event_id, locale, user_id, original_id, action, happened_at) values (%d, %s, %d, %d, %s, %s)",
+                                               array(
+                                                       // Start unique key.
+                                                       'event_id'    => $event->id(),
+                                                       'locale'      => $translation_set->locale,
+                                                       'user_id'     => $user_id,
+                                                       'original_id' => $translation->original_id,
+                                                       // End unique key.
+                                                       'action'      => $action,
+                                                       'happened_at' => $happened_at->format( 'Y-m-d H:i:s' ),
+                                               ),
+                                       ),
+                               );
+                               // phpcs:enable
+                       }
+               } catch ( Exception $exception ) {
+                       // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+                       error_log( $exception );
+               }
+       }
+
+       /**
+        * Get active events at a given time.
+        *
+        * @return Event[]
+        * @throws Exception When it fails to get active events.
+        */
+       private function get_active_events( DateTimeImmutable $at ): array {
+               $events = $this->active_events_cache->get();
+               if ( null === $events ) {
+                       $cache_duration = Active_Events_Cache::CACHE_DURATION;
+                       $boundary_start = $at;
+                       $boundary_end   = $at->modify( "+$cache_duration seconds" );
+
+                       // Get events for which start is before $boundary_end AND end is after $boundary_start.
+                       $event_ids = get_posts(
+                               array(
+                                       'post_type'      => Translation_Events::CPT,
+                                       'post_status'    => 'publish',
+                                       'posts_per_page' => - 1,
+                                       'fields'         => 'ids',
+                                       'meta_query'     => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+                                               array(
+                                                       'key'     => '_event_start',
+                                                       'value'   => $boundary_end->format( 'Y-m-d H:i:s' ),
+                                                       'compare' => '<',
+                                                       'type'    => 'DATETIME',
+                                               ),
+                                               array(
+                                                       'key'     => '_event_end',
+                                                       'value'   => $boundary_start->format( 'Y-m-d H:i:s' ),
+                                                       'compare' => '>',
+                                                       'type'    => 'DATETIME',
+                                               ),
+                                       ),
+                               ),
+                       );
+
+                       $events = array();
+                       foreach ( $event_ids as $event_id ) {
+                               $meta     = get_post_meta( $event_id );
+                               $events[] = Event::from_post_meta( $event_id, $meta );
+                       }
+
+                       $this->active_events_cache->cache( $events );
+               }
+
+               // Filter out events that aren't actually active at $at.
+               return array_filter(
+                       $events,
+                       function ( $event ) use ( $at ) {
+                               return $event->start() <= $at && $at <= $event->end();
+                       }
+               );
+       }
+
+       /**
+        * Filter an array of events so that it only includes events the given user is attending.
+        *
+        * @param Event[] $events Events.
+        *
+        * @return Event[]
+        */
+       // phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
+       // phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.Found
+       private function select_events_user_is_registered_for( array $events, int $user_id ): array {
+               $attending_event_ids = get_user_meta( $user_id, Route::USER_META_KEY_ATTENDING, true );
+               return array_filter(
+                       $events,
+                       function ( Event $event ) use ( $attending_event_ids ) {
+                               return isset( $attending_event_ids[ $event->id() ] );
+                       }
+               );
+       }
+       // phpcs:enable
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/includes/stats-listener.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateseventphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/event.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/event.php                          (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/event.php    2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,121 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Template for event page.
+ */
+
+namespace Wporg\TranslationEvents;
+
+use WP_Post;
+
+/** @var WP_Post $event */
+/** @var int $event_id */
+/** @var string $event_title */
+/** @var string $event_description */
+/** @var string $event_start */
+/** @var string $event_end */
+/** @var bool $user_is_attending */
+/** @var Event_Stats $event_stats */
+
+/* translators: %s: Event title. */
+gp_title( sprintf( __( 'Translation Events - %s' ), esc_html( $event_title ) ) );
+gp_breadcrumb_translation_events( array( esc_html( $event_title ) ) );
+gp_tmpl_header();
+gp_tmpl_load( 'events-header', get_defined_vars(), __DIR__ );
+?>
+
+<div class="event-page-wrapper">
+       <div class="event-details-head">
+               <h1>
+                       <?php echo esc_html( $event_title ); ?>
+                       <?php if ( 'draft' === $event->post_status ) : ?>
+                               <span class="event-label-draft"><?php echo esc_html( $event->post_status ); ?></span>
+                       <?php endif; ?>
+               </h1>
+               <p>
+                       Host: <a href="<?php echo esc_attr( get_author_posts_url( $event->post_author ) ); ?>"><?php echo esc_html( get_the_author_meta( 'display_name', $event->post_author ) ); ?></a>
+                       <?php if ( current_user_can( 'edit_post', $event_id ) ) : ?>
+                               <a class="event-page-edit-link button" href="<?php echo esc_url( gp_url( 'events/edit/' . $event_id ) ); ?>"><span class="dashicons dashicons-edit"></span>Edit event</a>
+                       <?php endif ?>
+               </p>
+       </div>
+       <div class="event-details-left">
+               <div class="event-page-content">
+                       <?php
+                               echo wp_kses_post( wpautop( make_clickable( $event_description ) ) );
+                       ?>
+               </div>
+               <?php if ( ! empty( $event_stats->rows() ) ) : ?>
+       <div class="event-details-stats">
+               <h2>Stats</h2>
+               <table>
+                       <thead>
+                       <tr>
+                               <th scope="col">Locale</th>
+                               <th scope="col">Translations created</th>
+                               <th scope="col">Translations reviewed</th>
+                               <th scope="col">Contributors</th>
+                       </tr>
+                       </thead>
+                       <tbody>
+                       <?php /** @var $row Stats_Row */ ?>
+                       <?php foreach ( $event_stats->rows() as $locale_ => $row ) : ?>
+                       <tr>
+                               <td><?php echo esc_html( $locale_ ); ?></td>
+                               <td><?php echo esc_html( $row->created ); ?></td>
+                               <td><?php echo esc_html( $row->reviewed ); ?></td>
+                               <td><?php echo esc_html( $row->users ); ?></td>
+                       </tr>
+               <?php endforeach ?>
+                       <tr class="event-details-stats-totals">
+                               <td>Total</td>
+                               <td><?php echo esc_html( $event_stats->totals()->created ); ?></td>
+                               <td><?php echo esc_html( $event_stats->totals()->reviewed ); ?></td>
+                               <td><?php echo esc_html( $event_stats->totals()->users ); ?></td>
+                       </tr>
+                       </tbody>
+               </table>
+       </div>
+       <details class="event-stats-summary">
+               <summary>View stats summary in text </summary>
+               <p class="event-stats-text"><?php echo esc_html( sprintf( 'At the %s event, %d people contributed in %d languages (%s), translated %d strings and reviewed %d strings.', esc_html( $event_title ), esc_html( $event_stats->totals()->users ), count( $event_stats->rows() ), esc_html( implode( ',', array_keys( $event_stats->rows() ) ) ), esc_html( $event_stats->totals()->created ), esc_html( $event_stats->totals()->reviewed ) ) ); ?></p>
+       </details>
+       <?php endif ?>
+       </div>
+       <div class="event-details-right">
+               <div class="event-details-date">
+                       <p>
+                               <span class="event-details-date-label">Starts:</span> <time class="event-utc-time" datetime="<?php echo esc_attr( $event_start ); ?>"></time>
+                               <span class="event-details-date-label">Ends:</span><time class="event-utc-time" datetime="<?php echo esc_attr( $event_end ); ?>"></time>
+                       </p>
+               </div>
+               <?php if ( is_user_logged_in() ) : ?>
+               <div class="event-details-join">
+                       <?php
+                       $current_time = gmdate( 'Y-m-d H:i:s' );
+                       if ( strtotime( $current_time ) > strtotime( $event_end ) ) :
+                               ?>
+                               <?php if ( $user_is_attending ) : ?>
+                                       <span class="event-details-join-expired"><?php esc_html_e( 'You attended', 'gp-translation-events' ); ?></span>
+                               <?php endif ?>
+                       <?php else : ?>
+                               <form class="event-details-attend" method="post" action="<?php echo esc_url( gp_url( "/events/attend/$event_id" ) ); ?>">
+                                       <?php if ( ! $user_is_attending ) : ?>
+                                               <input type="submit" class="button is-primary attend-btn" value="Attend Event"/>
+                                       <?php else : ?>
+                                               <input type="submit" class="button is-secondary attending-btn" value="You're attending"/>
+                                       <?php endif ?>
+                               </form>
+                       <?php endif ?>
+               </div>
+               <?php else : ?>
+               <div class="event-details-join">
+                       <p>
+                               <?php global $wp; ?>
+                               <a href="<?php echo esc_url( wp_login_url( home_url( $wp->request ) ) ); ?>" class="button is-primary attend-btn"><?php esc_html_e( 'Login to attend', 'gp-translation-events' ); ?></a>
+                       </p>
+               </div>
+               <?php endif; ?>
+       </div>
+</div>
+<div class="clear"></div>
+<?php gp_tmpl_footer(); ?>
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/event.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateseventsformphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-form.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-form.php                            (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-form.php      2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,114 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Template for event form.
+ */
+
+namespace Wporg\TranslationEvents;
+
+/** @var string $event_form_title */
+/** @var string $event_form_name */
+/** @var int $event_id */
+/** @var string $event_title */
+/** @var string $event_description */
+/** @var string $event_start */
+/** @var string $event_end */
+/** @var string $event_timezone */
+/** @var string $event_url */
+/** @var string $css_show_url */
+
+gp_title( __( 'Translation Events' ) . ' - ' . esc_html( $event_form_title . ' - ' . $event_title ) );
+gp_breadcrumb_translation_events( array( esc_html( $event_form_title ) ) );
+gp_tmpl_header();
+gp_tmpl_load( 'events-header', get_defined_vars(), __DIR__ );
+?>
+<div class="event-page-wrapper">
+<h2 class="event-page-title"><?php echo esc_html( $event_form_title ); ?></h2>
+<form class="translation-event-form" action="" method="post">
+       <?php wp_nonce_field( '_event_nonce', '_event_nonce' ); ?>
+       <input type="hidden" name="action" value="submit_event_ajax">
+       <input type="hidden" id="form-name" name="form_name" value="<?php echo esc_attr( $event_form_name ); ?>">
+       <input type="hidden" id="event-id" name="event_id" value="<?php echo esc_attr( $event_id ); ?>">
+       <input type="hidden" id="event-form-action" name="event_form_action">
+       <div>
+               <label for="event-title">Event Title</label>
+               <input type="text" id="event-title" name="event_title" value="<?php echo esc_html( $event_title ); ?>" required>
+       </div>
+       <div id="event-url" class="<?php echo esc_attr( $css_show_url ); ?>">
+               <label for="event-permalink">Event URL</label>
+               <a id="event-permalink" class="event-permalink" href="<?php echo esc_url( $event_url ); ?>" target="_blank"><?php echo esc_url( $event_url ); ?></a>
+       </div>
+       <div>
+               <label for="event-description">Event Description</label>
+               <textarea id="event-description" name="event_description" rows="4" required><?php echo esc_html( $event_description ); ?></textarea>
+       </div>
+       <div>
+               <label for="event-start">Start Date</label>
+               <input type="datetime-local" id="event-start" name="event_start" value="<?php echo esc_attr( $event_start ); ?>" required>
+       </div>
+       <div>
+               <label for="event-end">End Date</label>
+               <input type="datetime-local" id="event-end" name="event_end" value="<?php echo esc_attr( $event_end ); ?>" required>
+       </div>
+       <div>
+               <label for="event-timezone">Event Timezone</label>
+               <select id="event-timezone" name="event_timezone"  required>
+                       <?php
+                       echo wp_kses(
+                               wp_timezone_choice( $event_timezone, get_user_locale() ),
+                               array(
+                                       'optgroup' => array( 'label' => array() ),
+                                       'option'   => array(
+                                               'value'    => array(),
+                                               'selected' => array(),
+                                       ),
+                               )
+                       );
+                       ?>
+               </select>
+       </div>
+       <div class="submit-btn-group">
+               <label for="event-status"></label>
+       <?php if ( $event_id ) : ?>
+               <?php if ( isset( $event_status ) && 'draft' === $event_status ) : ?>
+                       <button class="button is-primary save-draft submit-event" type="submit" data-event-status="draft">Update Draft</button>
+               <?php endif; ?>
+       <button class="button is-primary submit-event" type="submit"  data-event-status="publish">
+               <?php echo ( isset( $event_status ) && 'publish' === $event_status ) ? esc_html( 'Update Event' ) : esc_html( 'Publish Event' ); ?>
+       </button>
+       <?php else : ?>
+               <button class="button is-primary save-draft submit-event" type="submit" data-event-status="draft">Save Draft</button>
+               <button class="button is-primary submit-event" type="submit"  data-event-status="publish">Publish Event</button>
+       <?php endif; ?>
+       <?php if ( isset( $create_delete_button ) && $create_delete_button ) : ?>
+               <button id="delete-button" class="button is-destructive delete-event" type="submit" name="submit" value="Delete" style="display: <?php echo esc_attr( $visibility_delete_button ); ?>">Delete Event</button>
+       <?php endif; ?>
+       </div>
+       <div class="clear"></div>
+       <div class="published-update-text">
+               <?php
+               $visibility_published_button = 'none';
+               if ( isset( $event_status ) && 'publish' === $event_status ) {
+                       $visibility_published_button = 'block';
+               }
+               ?>
+               <span id="published-update-text" style="display: <?php echo esc_attr( $visibility_published_button ); ?>">
+               <?php
+               $polyglots_slack_channel = 'https://wordpress.slack.com/archives/C02RP50LK';
+               echo wp_kses(
+               // translators: %s: Polyglots Slack channel URL.
+                       sprintf( __( 'If you need to update the event slug, please, contact with an admin in the <a href="%s" target="_blank">Polyglots</a> channel in Slack.', 'gp-translation-events' ), $polyglots_slack_channel ),
+                       array(
+                               'a' => array(
+                                       'href'   => array(),
+                                       'target' => array(),
+                               ),
+
+                       )
+               );
+               ?>
+               </span>
+       </div>
+</form>
+</div>
+<div class="clear"></div>
+<?php gp_tmpl_footer(); ?>
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-form.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateseventsheaderphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-header.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-header.php                          (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-header.php    2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,24 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+namespace Wporg\TranslationEvents;
+
+use GP;
+?>
+
+<div class="event-list-top-bar">
+       <ul class="event-list-nav">
+               <?php if ( is_user_logged_in() ) : ?>
+                       <li><a href="<?php echo esc_url( gp_url( '/events/my-events/' ) ); ?>">My Events</a></li>
+                       <?php
+                       /**
+                        * Filter the ability to create, edit, or delete an event.
+                        *
+                        * @param bool $can_crud_event Whether the user can create, edit, or delete an event.
+                        */
+                       $can_crud_event = apply_filters( 'gp_translation_events_can_crud_event', GP::$permission->current_user_can( 'admin' ) );
+                       if ( $can_crud_event ) :
+                               ?>
+                               <li><a class="button is-primary" href="<?php echo esc_url( gp_url( '/events/new/' ) ); ?>">Create Event</a></li>
+                       <?php endif; ?>
+               <?php endif; ?>
+       </ul>
+</div>
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-header.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateseventslistphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-list.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-list.php                            (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-list.php      2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,184 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Events list page.
+ */
+
+namespace Wporg\TranslationEvents;
+
+use DateTime;
+use WP_Query;
+
+/** @var WP_Query $current_events_query */
+/** @var WP_Query $upcoming_events_query */
+/** @var WP_Query $past_events_query */
+
+gp_title( __( 'Translation Events', 'gp-translation-events' ) );
+gp_breadcrumb_translation_events();
+gp_tmpl_header();
+gp_tmpl_load( 'events-header', get_defined_vars(), __DIR__ );
+?>
+
+<div class="event-page-wrapper">
+       <h1 class="event_page_title"><?php esc_html_e( 'Translation Events', 'gp-translation-events' ); ?></h1>
+<div class="event-left-col">
+<?php
+if ( $current_events_query->have_posts() ) :
+       ?>
+       <h2><?php esc_html_e( 'Current events', 'gp-translation-events' ); ?></h2>
+       <ul class="event-list">
+               <?php
+               while ( $current_events_query->have_posts() ) :
+                       $current_events_query->the_post();
+                       $event_end = Event::get_end_date_text( get_post_meta( get_the_ID(), '_event_end', true ) );
+                       $event_url = gp_url( wp_make_link_relative( get_the_permalink() ) );
+                       ?>
+                       <li class="event-list-item">
+                               <a href="<?php echo esc_url( $event_url ); ?>"><?php the_title(); ?></a>
+                               <span class="event-list-date"><?php echo esc_html( $event_end ); ?></span>
+                               <?php the_excerpt(); ?>
+                       </li>
+                       <?php
+               endwhile;
+               ?>
+       </ul>
+
+       <?php
+       echo wp_kses_post(
+               paginate_links(
+                       array(
+                               'total'     => $current_events_query->max_num_pages,
+                               'current'   => max( 1, $current_events_query->query_vars['paged'] ),
+                               'format'    => '?current_events_paged=%#%',
+                               'prev_text' => '&laquo; Previous',
+                               'next_text' => 'Next &raquo;',
+                       )
+               ) ?? ''
+       );
+
+       wp_reset_postdata();
+endif;
+if ( $upcoming_events_query->have_posts() ) :
+       ?>
+       <h2><?php esc_html_e( 'Upcoming events', 'gp-translation-events' ); ?></h2>
+       <ul class="event-list">
+               <?php
+               while ( $upcoming_events_query->have_posts() ) :
+                       $upcoming_events_query->the_post();
+                       $event_start = ( new DateTime( get_post_meta( get_the_ID(), '_event_start', true ) ) )->format( 'l, F j, Y' );
+                       ?>
+                       <li class="event-list-item">
+                               <a href="<?php echo esc_url( gp_url( wp_make_link_relative( get_the_permalink() ) ) ); ?>"><?php the_title(); ?></a>
+                               <span class="event-list-date"><?php echo esc_html( $event_start ); ?></span>
+                               <?php the_excerpt(); ?>
+                       </li>
+                       <?php
+               endwhile;
+               ?>
+       </ul>
+
+       <?php
+       echo wp_kses_post(
+               paginate_links(
+                       array(
+                               'total'     => $upcoming_events_query->max_num_pages,
+                               'current'   => max( 1, $upcoming_events_query->query_vars['paged'] ),
+                               'format'    => '?upcoming_events_paged=%#%',
+                               'prev_text' => '&laquo; Previous',
+                               'next_text' => 'Next &raquo;',
+                       )
+               ) ?? ''
+       );
+
+       wp_reset_postdata();
+endif;
+if ( $past_events_query->have_posts() ) :
+       ?>
+       <h2><?php esc_html_e( 'Past events', 'gp-translation-events' ); ?></h2>
+       <ul class="event-list">
+               <?php
+               while ( $past_events_query->have_posts() ) :
+                       $past_events_query->the_post();
+                       $event_start = ( new DateTime( get_post_meta( get_the_ID(), '_event_start', true ) ) )->format( 'M j, Y' );
+                       $event_end   = ( new DateTime( get_post_meta( get_the_ID(), '_event_end', true ) ) )->format( 'M j, Y' );
+                       ?>
+                       <li class="event-list-item">
+                               <a href="<?php echo esc_url( gp_url( wp_make_link_relative( get_the_permalink() ) ) ); ?>"><?php the_title(); ?></a>
+                               <?php if ( $event_start === $event_end ) : ?>
+                                       <span class="event-list-date"><?php echo esc_html( $event_start ); ?></span>
+                               <?php else : ?>
+                                       <span class="event-list-date"><?php echo esc_html( $event_start ); ?> - <?php echo esc_html( $event_end ); ?></span>
+                               <?php endif; ?>
+                               <?php the_excerpt(); ?>
+                       </li>
+                       <?php
+               endwhile;
+               ?>
+       </ul>
+
+       <?php
+       echo wp_kses_post(
+               paginate_links(
+                       array(
+                               'total'     => $past_events_query->max_num_pages,
+                               'current'   => max( 1, $past_events_query->query_vars['paged'] ),
+                               'format'    => '?past_events_paged=%#%',
+                               'prev_text' => '&laquo; Previous',
+                               'next_text' => 'Next &raquo;',
+                       )
+               ) ?? ''
+       );
+
+       wp_reset_postdata();
+endif;
+
+if ( 0 === $current_events_query->post_count && 0 === $upcoming_events_query->post_count && 0 === $past_events_query->post_count ) :
+       esc_html_e( 'No events found.', 'gp-translation-events' );
+endif;
+?>
+</div>
+<?php if ( is_user_logged_in() ) : ?>
+       <div class="event-right-col">
+               <h3 class="">Events I'm Attending</h3>
+               <?php if ( ! $user_attending_events_query->have_posts() ) : ?>
+                       <p>You don't have any events to attend.</p>
+               <?php else : ?>
+                       <ul class="event-attending-list">
+                               <?php
+                               while ( $user_attending_events_query->have_posts() ) :
+                                       $user_attending_events_query->the_post();
+                                       $event_start = ( new DateTime( get_post_meta( get_the_ID(), '_event_start', true ) ) )->format( 'M j, Y' );
+                                       $event_end   = ( new DateTime( get_post_meta( get_the_ID(), '_event_end', true ) ) )->format( 'M j, Y' );
+                                       ?>
+                                       <li class="event-list-item">
+                                               <a href="<?php echo esc_url( gp_url( wp_make_link_relative( get_the_permalink() ) ) ); ?>"><?php the_title(); ?></a>
+                                               <?php if ( $event_start === $event_end ) : ?>
+                                                       <span class="event-list-date events-i-am-attending"><?php echo esc_html( $event_start ); ?></span>
+                                               <?php else : ?>
+                                                       <span class="event-list-date events-i-am-attending"><?php echo esc_html( $event_start ); ?> - <?php echo esc_html( $event_end ); ?></span>
+                                               <?php endif; ?>
+                                       </li>
+                                       <?php
+                               endwhile;
+                               ?>
+                       </ul>
+                       <?php
+                               echo wp_kses_post(
+                                       paginate_links(
+                                               array(
+                                                       'total'     => $user_attending_events_query->max_num_pages,
+                                                       'current'   => max( 1, $user_attending_events_query->query_vars['paged'] ),
+                                                       'format'    => '?user_attending_events_paged=%#%',
+                                                       'prev_text' => '&laquo; Previous',
+                                                       'next_text' => 'Next &raquo;',
+                                               )
+                                       ) ?? ''
+                               );
+
+                               wp_reset_postdata();
+               endif;
+               ?>
+       </div>
+<?php endif; ?>
+</div>
+<div class="clear"></div>
+<?php gp_tmpl_footer(); ?>
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-list.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateseventsmyeventsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-my-events.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-my-events.php                               (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-my-events.php 2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,122 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Template for My Events.
+ */
+
+namespace Wporg\TranslationEvents;
+
+use DateTime;
+use WP_Query;
+
+/** @var WP_Query $events_i_created_query */
+/** @var WP_Query $events_i_attended_query */
+
+gp_title( esc_html__( 'Translation Events', 'gp-translation-events' ) . ' - ' . esc_html__( 'My Events', 'gp-translation-events' ) );
+gp_breadcrumb_translation_events( array( esc_html__( 'My Events', 'gp-translation-events' ) ) );
+gp_tmpl_header();
+gp_tmpl_load( 'events-header', get_defined_vars(), __DIR__ );
+?>
+
+<div class="event-page-wrapper">
+       <h1 class="event_page_title"><?php esc_html_e( 'My Events', 'gp-translation-events' ); ?> </h1>
+       <h2 class="event_page_title"><?php esc_html_e( 'Events I have created', 'gp-translation-events' ); ?> </h2>
+       <?php if ( $events_i_created_query->have_posts() ) : ?>
+               <ul>
+               <?php
+               while ( $events_i_created_query->have_posts() ) :
+                       $events_i_created_query->the_post();
+                       $event_id                      = get_the_ID();
+                       $event_start                   = get_post_meta( $event_id, '_event_start', true );
+                       list( $permalink, $post_name ) = get_sample_permalink( $event_id );
+                       $permalink                     = str_replace( '%pagename%', $post_name, $permalink );
+                       $event_url                     = gp_url( wp_make_link_relative( $permalink ) );
+                       $event_edit_url                = gp_url( 'events/edit/' . $event_id );
+                       $event_status                  = get_post_status( $event_id );
+                       $event_start                   = ( new DateTime( get_post_meta( get_the_ID(), '_event_start', true ) ) )->format( 'M j, Y' );
+                       $event_end                     = ( new DateTime( get_post_meta( get_the_ID(), '_event_end', true ) ) )->format( 'M j, Y' );
+                       ?>
+                       <li class="event-list-item">
+                               <a class="event-link-<?php echo esc_attr( $event_status ); ?>" href="<?php echo esc_url( $event_url ); ?>"><?php the_title(); ?></a>
+                               <a href="<?php echo esc_url( $event_edit_url ); ?>" class="button is-small action edit">Edit</a>
+                               <?php if ( 'draft' === $event_status ) : ?>
+                                       <span class="event-label-<?php echo esc_attr( $event_status ); ?>"><?php echo esc_html( $event_status ); ?></span>
+                               <?php endif; ?>
+                               <?php if ( $event_start === $event_end ) : ?>
+                                       <span class="event-list-date events-i-am-attending"><?php echo esc_html( $event_start ); ?></span>
+                               <?php else : ?>
+                                       <span class="event-list-date events-i-am-attending"><?php echo esc_html( $event_start ); ?> - <?php echo esc_html( $event_end ); ?></span>
+                               <?php endif; ?>
+                               <p><?php the_excerpt(); ?></p>
+                       </li>
+               <?php endwhile; ?>
+               </ul>
+
+               <?php
+               echo wp_kses_post(
+                       paginate_links(
+                               array(
+                                       'total'     => $events_i_created_query->max_num_pages,
+                                       'current'   => max( 1, $events_i_created_query->query_vars['events_i_created_paged'] ),
+                                       'format'    => '?events_i_created_paged=%#%',
+                                       'prev_text' => '&laquo; Previous',
+                                       'next_text' => 'Next &raquo;',
+                               )
+                       ) ?? ''
+               );
+
+               wp_reset_postdata();
+       else :
+               echo 'No events found.';
+       endif;
+       ?>
+
+       <h2 class="event_page_title"><?php esc_html_e( 'Events I attended', 'gp-translation-events' ); ?> </h2>
+       <?php if ( $events_i_attended_query->have_posts() ) : ?>
+               <ul>
+               <?php
+               while ( $events_i_attended_query->have_posts() ) :
+                       $events_i_attended_query->the_post();
+                       $event_id                      = get_the_ID();
+                       $event_start                   = get_post_meta( $event_id, '_event_start', true );
+                       list( $permalink, $post_name ) = get_sample_permalink( $event_id );
+                       $permalink                     = str_replace( '%pagename%', $post_name, $permalink );
+                       $event_url                     = gp_url( wp_make_link_relative( $permalink ) );
+                       $event_edit_url                = gp_url( 'events/edit/' . $event_id );
+                       $event_status                  = get_post_status( $event_id );
+                       $event_start                   = ( new DateTime( get_post_meta( get_the_ID(), '_event_start', true ) ) )->format( 'M j, Y' );
+                       $event_end                     = ( new DateTime( get_post_meta( get_the_ID(), '_event_end', true ) ) )->format( 'M j, Y' );
+                       ?>
+                       <li class="event-list-item">
+                               <a class="event-link-<?php echo esc_attr( $event_status ); ?>" href="<?php echo esc_url( $event_url ); ?>"><?php the_title(); ?></a>
+                               <?php if ( $event_start === $event_end ) : ?>
+                                       <span class="event-list-date events-i-am-attending"><?php echo esc_html( $event_start ); ?></span>
+                               <?php else : ?>
+                                       <span class="event-list-date events-i-am-attending"><?php echo esc_html( $event_start ); ?> - <?php echo esc_html( $event_end ); ?></span>
+                               <?php endif; ?>
+                               <p><?php the_excerpt(); ?></p>
+                       </li>
+               <?php endwhile; ?>
+               </ul>
+
+               <?php
+               echo wp_kses_post(
+                       paginate_links(
+                               array(
+                                       'total'     => $events_i_attended_query->max_num_pages,
+                                       'current'   => max( 1, $events_i_attended_query->query_vars['events_i_attended_paged'] ),
+                                       'format'    => '?events_i_attended_paged=%#%',
+                                       'prev_text' => '&laquo; Previous',
+                                       'next_text' => 'Next &raquo;',
+                               )
+                       ) ?? ''
+               );
+
+               wp_reset_postdata();
+       else :
+               echo 'No events found.';
+       endif;
+       ?>
+</div>
+<?php
+       gp_tmpl_footer();
+?>
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/events-my-events.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventstemplateshelperfunctionsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/helper-functions.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/helper-functions.php                               (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/helper-functions.php 2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,17 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Get event breadcrumb.
+ *
+ * @param array $extra_items   Array of additional items to add to the breadcrumb.
+ *
+ * @return string   HTML of the breadcrumb.
+ */
+function gp_breadcrumb_translation_events( $extra_items = array() ) {
+       $breadcrumb = array(
+               empty( $extra_items ) ? __( 'Events', 'gp-translation-events' ) : gp_link_get( gp_url( '/events' ), __( 'Events', 'gp-translation-events' ) ),
+       );
+       if ( ! empty( $extra_items ) ) {
+               $breadcrumb = array_merge( $breadcrumb, $extra_items );
+       }
+       return gp_breadcrumb( $breadcrumb );
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/templates/helper-functions.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggptranslationeventswporggptranslationeventsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/wporg-gp-translation-events.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/wporg-gp-translation-events.php                              (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/wporg-gp-translation-events.php        2024-03-04 11:41:16 UTC (rev 13268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,541 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Plugin Name: Translation Events
+ * Plugin URI: https://github.com/WordPress/wporg-gp-translation-events/
+ * Description: A WordPress plugin for creating translation events.
+ * Version: 1.0.0
+ * Requires at least: 6.4
+ * Tested up to: 6.4
+ * Requires PHP: 7.4
+ * Author: WordPress Contributors
+ * Author URI: https://github.com/WordPress/wporg-gp-translation-events/
+ * License: GPLv2 or later
+ * License URI: http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * Text Domain: gp-translation-events
+ *
+ * @package Translation Events
+ */
+
+namespace Wporg\TranslationEvents;
+
+use DateTime;
+use DateTimeZone;
+use Exception;
+use GP;
+use WP_Post;
+use WP_Query;
+
+class Translation_Events {
+       const CPT = 'translation_event';
+
+       public static function get_instance() {
+               static $instance = null;
+               if ( null === $instance ) {
+                       $instance = new self();
+               }
+               return $instance;
+       }
+
+       public function __construct() {
+               \add_action( 'wp_ajax_submit_event_ajax', array( $this, 'submit_event_ajax' ) );
+               \add_action( 'wp_ajax_nopriv_submit_event_ajax', array( $this, 'submit_event_ajax' ) );
+               \add_action( 'wp_enqueue_scripts', array( $this, 'register_translation_event_js' ) );
+               \add_action( 'init', array( $this, 'register_event_post_type' ) );
+               \add_action( 'add_meta_boxes', array( $this, 'event_meta_boxes' ) );
+               \add_action( 'save_post', array( $this, 'save_event_meta_boxes' ) );
+               \add_action( 'transition_post_status', array( $this, 'event_status_transition' ), 10, 3 );
+               \add_filter( 'gp_nav_menu_items', array( $this, 'gp_event_nav_menu_items' ), 10, 2 );
+               \add_filter( 'wp_insert_post_data', array( $this, 'generate_event_slug' ), 10, 2 );
+               \add_action( 'gp_init', array( $this, 'gp_init' ) );
+               \add_action( 'gp_before_translation_table', array( $this, 'add_active_events_current_user' ) );
+               \register_activation_hook( __FILE__, array( $this, 'activate' ) );
+       }
+
+       public function gp_init() {
+               require_once __DIR__ . '/templates/helper-functions.php';
+               require_once __DIR__ . '/includes/active-events-cache.php';
+               require_once __DIR__ . '/includes/event.php';
+               require_once __DIR__ . '/includes/route.php';
+               require_once __DIR__ . '/includes/stats-calculator.php';
+               require_once __DIR__ . '/includes/stats-listener.php';
+
+               GP::$router->add( '/events?', array( 'Wporg\TranslationEvents\Route', 'events_list' ) );
+               GP::$router->add( '/events/new', array( 'Wporg\TranslationEvents\Route', 'events_create' ) );
+               GP::$router->add( '/events/edit/(\d+)', array( 'Wporg\TranslationEvents\Route', 'events_edit' ) );
+               GP::$router->add( '/events/attend/(\d+)', array( 'Wporg\TranslationEvents\Route', 'events_attend' ), 'post' );
+               GP::$router->add( '/events/my-events', array( 'Wporg\TranslationEvents\Route', 'my_events' ) );
+               GP::$router->add( '/events/([a-z0-9_-]+)', array( 'Wporg\TranslationEvents\Route', 'events_details' ) );
+
+               $active_events_cache = new Active_Events_Cache();
+               $stats_listener      = new Stats_Listener( $active_events_cache );
+               $stats_listener->start();
+       }
+
+       public function activate() {
+               global $wpdb;
+               $create_table = "
+               CREATE TABLE `{$wpdb->base_prefix}event_actions` (
+                       `translate_event_actions_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+                       `event_id` int(10) NOT NULL COMMENT 'Post_ID of the translation_event post in the wp_posts table',
+                       `original_id` int(10) NOT NULL COMMENT 'ID of the translation',
+                       `user_id` int(10) NOT NULL COMMENT 'ID of the user who made the action',
+                       `action` enum('approve','create','reject','request_changes') NOT NULL COMMENT 'The action that the user made (create, reject, etc)',
+                       `locale` varchar(10) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL COMMENT 'Locale of the translation',
+                       `happened_at` datetime NOT NULL COMMENT 'When the action happened, in UTC',
+               PRIMARY KEY (`translate_event_actions_id`),
+               UNIQUE KEY `event_per_translated_original_per_user` (`event_id`,`locale`,`original_id`,`user_id`)
+               ) COMMENT='Tracks translation actions that happened during a translation event'";
+               require_once ABSPATH . 'wp-admin/includes/upgrade.php';
+               dbDelta( $create_table );
+       }
+
+       /**
+        * Register the event post type.
+        */
+       public function register_event_post_type() {
+               $labels = array(
+                       'name'               => 'Translation Events',
+                       'singular_name'      => 'Translation Event',
+                       'menu_name'          => 'Translation Events',
+                       'add_new'            => 'Add New',
+                       'add_new_item'       => 'Add New Translation Event',
+                       'edit_item'          => 'Edit Translation Event',
+                       'new_item'           => 'New Translation Event',
+                       'view_item'          => 'View Translation Event',
+                       'search_items'       => 'Search Translation Events',
+                       'not_found'          => 'No translation events found',
+                       'not_found_in_trash' => 'No translation events found in trash',
+               );
+
+               $args = array(
+                       'labels'      => $labels,
+                       'public'      => true,
+                       'has_archive' => true,
+                       'menu_icon'   => 'dashicons-calendar',
+                       'supports'    => array( 'title', 'editor', 'thumbnail', 'revisions' ),
+                       'rewrite'     => array( 'slug' => 'events' ),
+                       'show_ui'     => false,
+               );
+
+               register_post_type( self::CPT, $args );
+       }
+       /**
+        * Add meta boxes for the event post type.
+        */
+       public function event_meta_boxes() {
+               \add_meta_box( 'event_dates', 'Event Dates', array( $this, 'event_dates_meta_box' ), self::CPT, 'normal', 'high' );
+       }
+
+       /**
+        * Output the event dates meta box.
+        *
+        * @param  WP_Post $post The current post object.
+        */
+       public function event_dates_meta_box( WP_Post $post ) {
+               wp_nonce_field( 'event_dates_nonce', 'event_dates_nonce' );
+               $event_start = get_post_meta( $post->ID, '_event_start', true );
+               $event_end   = get_post_meta( $post->ID, '_event_end', true );
+               echo '<label for="event_start">Start Date: </label>';
+               echo '<input type="date" id="event_start" name="event_start" value="' . esc_attr( $event_start ) . '" required>';
+               echo '<label for="event_end">End Date: </label>';
+               echo '<input type="date" id="event_end" name="event_end" value="' . esc_attr( $event_end ) . '" required>';
+       }
+
+       /**
+        * Save the event meta boxes.
+        *
+        * @param  int $post_id The current post ID.
+        */
+       public function save_event_meta_boxes( int $post_id ) {
+               $nonces = array( 'event_dates' );
+               foreach ( $nonces as $nonce ) {
+                       $nonce_name = $nonce . '_nonce';
+                       if ( ! isset( $_POST[ $nonce_name ] ) ) {
+                               return;
+                       }
+                       $nonce_value = sanitize_text_field( wp_unslash( $_POST[ $nonce_name ] ) );
+                       if ( ! wp_verify_nonce( $nonce_value, $nonce_name ) ) {
+                               return;
+                       }
+               }
+
+               $fields = array( 'event_start', 'event_end' );
+               foreach ( $fields as $field ) {
+                       if ( isset( $_POST[ $field ] ) ) {
+                               update_post_meta( $post_id, '_' . $field, sanitize_text_field( wp_unslash( $_POST[ $field ] ) ) );
+                       }
+               }
+       }
+
+       /**
+        * Validate the event dates.
+        *
+        * @param string $event_start The event start date.
+        * @param string $event_end The event end date.
+        * @return bool Whether the event dates are valid.
+        * @throws Exception When dates are invalid.
+        */
+       public function validate_event_dates( string $event_start, string $event_end ): bool {
+               if ( ! $event_start || ! $event_end ) {
+                       return false;
+               }
+               $event_start = new DateTime( $event_start );
+               $event_end   = new DateTime( $event_end );
+               if ( $event_start < $event_end ) {
+                       return true;
+               }
+               return false;
+       }
+
+       /**
+        * Handle the event form submission for the creation, editing, and deletion of events. This function is called via AJAX.
+        */
+       public function submit_event_ajax() {
+               if ( ! is_user_logged_in() ) {
+                       wp_send_json_error( esc_html__( 'The user must be logged in.', 'gp-translation-events' ), 403 );
+               }
+               $action           = isset( $_POST['form_name'] ) ? sanitize_text_field( wp_unslash( $_POST['form_name'] ) ) : '';
+               $event_id         = null;
+               $event            = null;
+               $response_message = '';
+               $form_actions     = array( 'draft', 'publish', 'delete' );
+               $is_nonce_valid   = false;
+               $nonce_name       = '_event_nonce';
+               if ( ! in_array( $action, array( 'create_event', 'edit_event', 'delete_event' ), true ) ) {
+                       wp_send_json_error( esc_html__( 'Invalid form name.', 'gp-translation-events' ), 403 );
+               }
+               /**
+                * Filter the ability to create, edit, or delete an event.
+                *
+                * @param bool $can_crud_event Whether the user can create, edit, or delete an event.
+                */
+               $can_crud_event = apply_filters( 'gp_translation_events_can_crud_event', GP::$permission->current_user_can( 'admin' ) );
+               if ( 'create_event' === $action && ( ! $can_crud_event ) ) {
+                       wp_send_json_error( esc_html__( 'The user does not have permission to create an event.', 'gp-translation-events' ), 403 );
+               }
+               if ( 'edit_event' === $action ) {
+                       $event_id = isset( $_POST['event_id'] ) ? sanitize_text_field( wp_unslash( $_POST['event_id'] ) ) : '';
+                       $event    = get_post( $event_id );
+                       if ( ! ( $can_crud_event || current_user_can( 'edit_post', $event_id ) || intval( $event->post_author ) === get_current_user_id() ) ) {
+                               wp_send_json_error( esc_html__( 'The user does not have permission to edit or delete the event.', 'gp-translation-events' ), 403 );
+                       }
+               }
+               if ( 'delete_event' === $action ) {
+                       $event_id = isset( $_POST['event_id'] ) ? sanitize_text_field( wp_unslash( $_POST['event_id'] ) ) : '';
+                       $event    = get_post( $event_id );
+                       if ( ! ( $can_crud_event || current_user_can( 'delete_post', $event->ID ) || get_current_user_id() === $event->post_author ) ) {
+                               wp_send_json_error( esc_html__( 'You do not have permission to delete this event.', 'gp-translation-events' ), 403 );
+                       }
+               }
+               if ( isset( $_POST[ $nonce_name ] ) ) {
+                       $nonce_value = sanitize_text_field( wp_unslash( $_POST[ $nonce_name ] ) );
+                       if ( wp_verify_nonce( $nonce_value, $nonce_name ) ) {
+                               $is_nonce_valid = true;
+                       }
+               }
+               if ( ! $is_nonce_valid ) {
+                       wp_send_json_error( esc_html__( 'Nonce verification failed.', 'gp-translation-events' ), 403 );
+               }
+               // This is a list of slugs that are not allowed, as they conflict with the event URLs.
+               $invalid_slugs = array( 'new', 'edit', 'attend', 'my-events' );
+               $title         = isset( $_POST['event_title'] ) ? sanitize_text_field( wp_unslash( $_POST['event_title'] ) ) : '';
+               // This will be sanitized by santitize_post which is called in wp_insert_post.
+               $description    = isset( $_POST['event_description'] ) ? force_balance_tags( wp_unslash( $_POST['event_description'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+               $event_start    = isset( $_POST['event_start'] ) ? sanitize_text_field( wp_unslash( $_POST['event_start'] ) ) : '';
+               $event_end      = isset( $_POST['event_end'] ) ? sanitize_text_field( wp_unslash( $_POST['event_end'] ) ) : '';
+               $event_timezone = isset( $_POST['event_timezone'] ) ? sanitize_text_field( wp_unslash( $_POST['event_timezone'] ) ) : '';
+               if ( isset( $title ) && in_array( sanitize_title( $title ), $invalid_slugs, true ) ) {
+                       wp_send_json_error( esc_html__( 'Invalid slug.', 'gp-translation-events' ), 422 );
+               }
+
+               $is_valid_event_date = false;
+               try {
+                       $is_valid_event_date = $this->validate_event_dates( $event_start, $event_end );
+               } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
+                       // Deliberately ignored, handled below.
+               }
+               if ( ! $is_valid_event_date ) {
+                       wp_send_json_error( esc_html__( 'Invalid event dates.', 'gp-translation-events' ), 422 );
+               }
+
+               $event_status = '';
+               if ( isset( $_POST['event_form_action'] ) && in_array( $_POST['event_form_action'], $form_actions, true ) ) {
+                       $event_status = sanitize_text_field( wp_unslash( $_POST['event_form_action'] ) );
+               }
+
+               if ( ! isset( $_POST['form_name'] ) ) {
+                       wp_send_json_error( esc_html__( 'Form name must be set.', 'gp-translation-events' ), 422 );
+               }
+
+               if ( 'create_event' === $action ) {
+                       $event_id         = wp_insert_post(
+                               array(
+                                       'post_type'    => self::CPT,
+                                       'post_title'   => $title,
+                                       'post_content' => $description,
+                                       'post_status'  => $event_status,
+                               )
+                       );
+                       $response_message = esc_html__( 'Event created successfully!', 'gp-translation-events' );
+               }
+               if ( 'edit_event' === $action ) {
+                       if ( ! isset( $_POST['event_id'] ) ) {
+                               wp_send_json_error( esc_html__( 'Event id is required.', 'gp-translation-events' ), 422 );
+                       }
+                       $event_id = sanitize_text_field( wp_unslash( $_POST['event_id'] ) );
+                       $event    = get_post( $event_id );
+                       if ( ! $event || self::CPT !== $event->post_type || ! ( current_user_can( 'edit_post', $event->ID ) || intval( $event->post_author ) === get_current_user_id() ) ) {
+                               wp_send_json_error( esc_html__( 'Event does not exist.', 'gp-translation-events' ), 404 );
+                       }
+                       wp_update_post(
+                               array(
+                                       'ID'           => $event_id,
+                                       'post_title'   => $title,
+                                       'post_content' => $description,
+                                       'post_status'  => $event_status,
+                               )
+                       );
+                       $response_message = esc_html__( 'Event updated successfully!', 'gp-translation-events' );
+               }
+               if ( 'delete_event' === $action ) {
+                       $event_id = sanitize_text_field( wp_unslash( $_POST['event_id'] ) );
+                       $event    = get_post( $event_id );
+                       if ( ! $event || self::CPT !== $event->post_type ) {
+                               wp_send_json_error( esc_html__( 'Event does not exist.', 'gp-translation-events' ), 404 );
+                       }
+                       if ( ! ( current_user_can( 'delete_post', $event->ID ) || get_current_user_id() === $event->post_author ) ) {
+                               wp_send_json_error( 'You do not have permission to delete this event' );
+                       }
+                       $stats_calculator = new Stats_Calculator();
+                       try {
+                               $event_stats = $stats_calculator->for_event( $event );
+                       } catch ( Exception $e ) {
+                               wp_send_json_error( esc_html__( 'Failed to calculate event stats.', 'gp-translation-events' ), 500 );
+                       }
+                       if ( ! empty( $event_stats->rows() ) ) {
+                               wp_send_json_error( esc_html__( 'Event has translations and cannot be deleted.', 'gp-translation-events' ), 422 );
+                       }
+                       wp_trash_post( $event_id );
+                       $response_message = esc_html__( 'Event deleted successfully!', 'gp-translation-events' );
+               }
+               if ( ! $event_id ) {
+                       wp_send_json_error( esc_html__( 'Event could not be created or updated.', 'gp-translation-events' ), 422 );
+               }
+               if ( 'delete_event' !== $_POST['form_name'] ) {
+                       try {
+                               update_post_meta( $event_id, '_event_start', $this->convert_to_utc( $event_start, $event_timezone ) );
+                               update_post_meta( $event_id, '_event_end', $this->convert_to_utc( $event_end, $event_timezone ) );
+                       } catch ( Exception $e ) {
+                               wp_send_json_error( esc_html__( 'Invalid start or end', 'gp-translation-events' ), 422 );
+                       }
+
+                       update_post_meta( $event_id, '_event_timezone', $event_timezone );
+               }
+               try {
+                       Active_Events_Cache::invalidate();
+               } catch ( Exception $e ) {
+                       // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+                       error_log( $e );
+               }
+
+               list( $permalink, $post_name ) = get_sample_permalink( $event_id );
+               $permalink                     = str_replace( '%pagename%', $post_name, $permalink );
+               wp_send_json_success(
+                       array(
+                               'message'        => $response_message,
+                               'eventId'        => $event_id,
+                               'eventUrl'       => str_replace( '%pagename%', $post_name, $permalink ),
+                               'eventStatus'    => $event_status,
+                               'eventEditUrl'   => esc_url( gp_url( '/events/edit/' . $event_id ) ),
+                               'eventDeleteUrl' => esc_url( gp_url( '/events/my-events/' ) ),
+                       )
+               );
+       }
+
+
+
+
+       /**
+        * Convert a date time in a time zone to UTC.
+        *
+        * @param string $date_time The date time in the time zone.
+        * @param string $time_zone The time zone.
+        * @return string The date time in UTC.
+        * @throws Exception When dates are invalid.
+        */
+       public function convert_to_utc( string $date_time, string $time_zone ): string {
+               $date_time = new DateTime( $date_time, new DateTimeZone( $time_zone ) );
+               $date_time->setTimezone( new DateTimeZone( 'UTC' ) );
+               return $date_time->format( 'Y-m-d H:i:s' );
+       }
+
+       public function register_translation_event_js() {
+               wp_register_style( 'translation-events-css', plugins_url( 'assets/css/translation-events.css', __FILE__ ), array(), filemtime( __DIR__ . '/assets/css/translation-events.css' ) );
+               gp_enqueue_style( 'translation-events-css' );
+               wp_register_script( 'translation-events-js', plugins_url( 'assets/js/translation-events.js', __FILE__ ), array( 'jquery', 'gp-common' ), filemtime( __DIR__ . '/assets/js/translation-events.js' ), false );
+               gp_enqueue_script( 'translation-events-js' );
+               wp_localize_script(
+                       'translation-events-js',
+                       '$translation_event',
+                       array(
+                               'url'          => admin_url( 'admin-ajax.php' ),
+                               '_event_nonce' => wp_create_nonce( self::CPT ),
+                       )
+               );
+       }
+
+       /**
+        * Handle the event status transition.
+        *
+        * The user who creates the event will assist to it when it's published.
+        *
+        * @param string  $new_status The new post status.
+        * @param string  $old_status The old post status.
+        * @param WP_Post $post       The post object.
+        */
+       public function event_status_transition( string $new_status, string $old_status, WP_Post $post ): void {
+               if ( self::CPT !== $post->post_type ) {
+                       return;
+               }
+               if ( 'publish' === $new_status && ( 'new' === $old_status || 'draft' === $old_status ) ) {
+                       $current_user_id         = get_current_user_id();
+                       $user_attending_events   = get_user_meta( $current_user_id, Route::USER_META_KEY_ATTENDING, true ) ?: array();
+                       $is_user_attending_event = in_array( $post->ID, $user_attending_events, true );
+                       if ( ! $is_user_attending_event ) {
+                               $new_user_attending_events              = $user_attending_events;
+                               $new_user_attending_events[ $post->ID ] = true;
+                               update_user_meta( $current_user_id, Route::USER_META_KEY_ATTENDING, $new_user_attending_events, $user_attending_events );
+                       }
+               }
+       }
+
+       /**
+        * Add the events link to the GlotPress main menu.
+        *
+        * @param array  $items    The menu items.
+        * @param string $location The menu location.
+        * @return array The modified menu items.
+        */
+       public function gp_event_nav_menu_items( array $items, string $location ): array {
+               if ( 'main' !== $location ) {
+                       return $items;
+               }
+               $new[ esc_url( gp_url( '/events/' ) ) ] = esc_html__( 'Events', 'gp-translation-events' );
+               return array_merge( $items, $new );
+       }
+
+       /**
+        * Generate a slug for the event post type when we save a draft event or when we publish an event.
+        *
+        * Generate a slug based on the event title if:
+        * - The event is a draft.
+        * - The event is published and it was a draft just before.
+        *
+        * @param array $data    An array of slashed post data.
+        * @param array $postarr An array of sanitized, but otherwise unmodified post data.
+        * @return array The modified post data.
+        */
+       public function generate_event_slug( array $data, array $postarr ): array {
+               if ( self::CPT === $data['post_type'] ) {
+                       if ( 'draft' === $data['post_status'] ) {
+                               $data['post_name'] = sanitize_title( $data['post_title'] );
+                       }
+                       if ( 'publish' === $data['post_status'] ) {
+                               if ( is_numeric( $postarr['ID'] ) && 0 !== $postarr['ID'] ) {
+                                       $post = get_post( $postarr['ID'] );
+                                       if ( $post instanceof WP_Post ) {
+                                               if ( 'draft' === $post->post_status ) {
+                                                       $data['post_name'] = sanitize_title( $data['post_title'] );
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               return $data;
+       }
+
+       /**
+        * Add the active events for the current user before the translation table.
+        *
+        * @return void
+        */
+       public function add_active_events_current_user(): void {
+               $user_attending_events      = get_user_meta( get_current_user_id(), Route::USER_META_KEY_ATTENDING, true ) ?: array();
+               $current_datetime_utc       = ( new DateTime( 'now', new DateTimeZone( 'UTC' ) ) )->format( 'Y-m-d H:i:s' );
+               $user_attending_events_args = array(
+                       'post_type'   => self::CPT,
+                       'post__in'    => array_keys( $user_attending_events ),
+                       'post_status' => 'publish',
+                       // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+                       'meta_query'  => array(
+                               array(
+                                       'key'     => '_event_start',
+                                       'value'   => $current_datetime_utc,
+                                       'compare' => '<=',
+                                       'type'    => 'DATETIME',
+                               ),
+                               array(
+                                       'key'     => '_event_end',
+                                       'value'   => $current_datetime_utc,
+                                       'compare' => '>=',
+                                       'type'    => 'DATETIME',
+                               ),
+                       ),
+                       // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+                       'meta_key'    => '_event_start',
+                       'orderby'     => 'meta_value',
+                       'order'       => 'ASC',
+               );
+               $user_attending_events_query = new WP_Query( $user_attending_events_args );
+               $number_of_events            = $user_attending_events_query->post_count;
+               if ( 0 === $number_of_events ) {
+                       return;
+               }
+
+               $content = '<div id="active-events-before-translation-table" class="active-events-before-translation-table">';
+               /* translators: %d: Number of events */
+               $content .= sprintf( _n( 'Contributing to %d event:', 'Contributing to %d events:', $number_of_events, 'gp-translation-events' ), $number_of_events );
+               $content .= '&nbsp;&nbsp;';
+               if ( $number_of_events > 3 ) {
+                       $counter = 0;
+                       while ( $user_attending_events_query->have_posts() && $counter < 2 ) {
+                               $user_attending_events_query->the_post();
+                               $url      = esc_url( gp_url( '/events/' . get_post_field( 'post_name', get_post() ) ) );
+                               $content .= '<span class="active-events-before-translation-table"><a href="' . $url . '" target="_blank">' . get_the_title() . '</a></span>';
+                               ++$counter;
+                       }
+
+                       $remaining_events = $number_of_events - 2;
+                       $url              = esc_url( gp_url( '/events/' ) );
+                       /* translators: %d: Number of remaining events */
+                       $content .= '<span class="remaining-events"><a href="' . $url . '" target="_blank">' . sprintf( esc_html__( ' and %d more events.', 'gp-translation-events' ), $remaining_events ) . '</a></span>';
+
+               } else {
+                       while ( $user_attending_events_query->have_posts() ) {
+                               $user_attending_events_query->the_post();
+                               $url      = esc_url( gp_url( '/events/' . get_post_field( 'post_name', get_post() ) ) );
+                               $content .= '<span class="active-events-before-translation-table"><a href="' . $url . '" target="_blank">' . get_the_title() . '</a></span>';
+                       }
+               }
+               $content .= '</div>';
+
+               echo wp_kses(
+                       $content,
+                       array(
+                               'div'  => array(
+                                       'id'    => array(),
+                                       'class' => array(),
+                               ),
+                               'span' => array(
+                                       'class' => array(),
+                               ),
+                               'a'    => array(
+                                       'href'   => array(),
+                                       'target' => array(),
+                               ),
+                       )
+               );
+       }
+}
+Translation_Events::get_instance();
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-translation-events/wporg-gp-translation-events.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span></div>

</body>
</html>