<!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>[11837] sites/trunk/wordpress.org/public_html/wp-content/plugins: Translate: Introduce the gp-translation-helpers plugin</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/11837">11837</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/11837","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>akirk</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2022-05-11 08:47:45 +0000 (Wed, 11 May 2022)</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: Introduce the gp-translation-helpers plugin

See <a href="http://meta.trac.wordpress.org/ticket/6310">#6310</a>, https://github.com/GlotPress/gp-translation-helpers/
props amieiro, spiraltee</pre>

<h3>Added Paths</h3>
<ul>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/</li>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/css/</li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelperscssdiscussioncss">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/css/discussion.css</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelperscsstranslationhelperscss">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/css/translation-helpers.css</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpersgptranslationhelpersphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/gp-translation-helpers.php</a></li>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers/</li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpershelpersbasehelperphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers/base-helper.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpershelpershelperotherlocalesphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers/helper-other-locales.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpershelpershelpertranslationdiscussionphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers/helper-translation-discussion.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpershelpershelpertranslationhistoryphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers/helper-translation-history.php</a></li>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers-assets/</li>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers-assets/css/</li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpershelpersassetscsstranslationdiscussioncss">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers-assets/css/translation-discussion.css</a></li>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers-assets/js/</li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpershelpersassetsjstranslationdiscussionjs">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers-assets/js/translation-discussion.js</a></li>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers-assets/templates/</li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpershelpersassetstemplatestranslationdiscussioncommentsphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers-assets/templates/translation-discussion-comments.php</a></li>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/</li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpersincludesclassgpnotificationsphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-gp-notifications.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpersincludesclassgproutetranslationhelpersphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-gp-route-translation-helpers.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpersincludesclassgptranslationhelpersphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-gp-translation-helpers.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpersincludesclassgthtemporarypostphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-gth-temporary-post.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpersincludesclasswporgnotificationsphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-wporg-notifications.php</a></li>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/js/</li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpersjsdiscussionjs">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/js/discussion.js</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpersjsrejectfeedbackjs">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/js/reject-feedback.js</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpersjstranslationhelpersjs">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/js/translation-helpers.js</a></li>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/templates/</li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelperstemplatescommentsectionphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/templates/comment-section.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelperstemplatesdiscussionphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/templates/discussion.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelperstemplatesoriginalpermalinktemplatephp">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/templates/original-permalink-template.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelperstemplatesoriginalpermalinkphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/templates/original-permalink.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelperstemplatestranslationhelpersphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/templates/translation-helpers.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggpcustomizationstemplatessettingseditphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-customizations/templates/settings-edit.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggpcustomizationstemplatessettingsphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-customizations/templates/settings.php</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/gp-translation-helpers
</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/gp-translation-helpers      2022-05-11 03:13:27 UTC (rev 11836)
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers       2022-05-11 08:47:45 UTC (rev 11837)
</ins><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpers"></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/gp-translation-helpers</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
+README.md
+composer.json
+composer.lock
+package-lock.json
+package.json
+phpcs.xml.dist
+.eslintrc.json
+tmp
</ins><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelperscssdiscussioncss"></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/gp-translation-helpers/css/discussion.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/gp-translation-helpers/css/discussion.css                                (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/css/discussion.css  2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,202 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+#original {
+       border: 1px solid black;
+}
+
+#original h1 {
+       margin: .5em 14px;
+       line-height: 34px;
+}
+
+#original p {
+       margin: 14px 26px 14px 14px;
+}
+
+#original ul#translation-list {
+       margin-left: 1rem;
+}
+
+.helper-translation-discussion {
+       position:relative;
+       min-height: 600px;
+}
+.discussion-list {
+       list-style:none;
+       max-width: 560px;
+}
+.comments-wrapper {
+       max-width: 600px;
+}
+article.comment {
+       margin: 15px 30px 15px 30px;
+       position: relative;
+       font-size: 0.9rem;
+}
+article.comment p {
+       margin-bottom: 0.5em;
+}
+article.comment footer {
+       overflow: hidden;
+       font-size: 0.8rem;
+       font-style: italic;
+}
+article.comment time {
+       font-style: italic;
+       opacity: 0.8;
+       display: inline-block;
+       padding-left: 4px;
+}
+.comment-locale {
+       opacity: 0.8;
+       float: right;
+}
+.comment-avatar {
+       margin-left: -45px;
+       margin-bottom: -25px;
+       width: 50px;
+       height: 26px;
+}
+.comment-avatar img {
+       display: block;
+       border-radius: 13px;
+}
+.comments-selector {
+       display: inline-block;
+       padding-left: 10px;
+       font-size: 0.9em;
+}
+.comment-content {
+       text-align: start;
+}
+.comment-form-comment label{
+       display: block;
+}
+h6 {
+       font-size: 1em;
+}
+.discussion-heading{
+       line-height: 1.2em;
+}
+.discussion-locales ul li{
+       display: inline;
+}
+.active-link{
+       font-weight: bold;
+}
+.discussion-wrapper textarea#comment {
+       width: 100%;
+       margin-top: 15px;
+}
+.discussion-wrapper .submit {
+       margin: 0 5px 0 0;
+       background: #2271b1;
+       border-color: #2271b1;
+       color: #fff;
+       text-decoration: none;
+       text-shadow: none;
+       font-size: 13px;
+       line-height: 2.15384615;
+       min-height: 30px;
+       padding: 0 10px;
+       cursor: pointer;
+       border-width: 1px;
+       border-style: solid;
+       -webkit-appearance: none;
+       border-radius: 3px;
+       white-space: nowrap;
+       box-sizing: border-box;
+}
+.discussion-wrapper .submit:hover {
+       background: #135e96;
+       border-color: #135e96;
+       color: #fff;
+}
+.modal-item {
+       display: inline-block;
+       width: 30%;
+       margin-top: 7px;
+}
+.modal-item label {
+       display: inherit;
+       margin-left: 8px;
+       cursor: pointer;
+}
+.modal-item input[type="checkbox"] {
+       transform: scale(1.5);
+       margin-right: 5px;
+}
+.modal-comment {
+       margin-top: 27px;
+}
+.modal-comment textarea {
+       width: 97%;
+       height: 170px;
+}
+.modal-btn {
+       float: right;
+       font-size: 15px;
+       height: 35px;
+       line-height: 1;
+       background: #0085ba;
+       border-color: #0073aa #006799 #006799;
+       box-shadow: 0 1px 0 #006799;
+       color: #fff;
+       text-decoration: none;
+       text-shadow: 0 -1px 1px #006799, 1px 0 1px #006799, 0 1px 1px #006799, -1px 0 1px #006799;
+       margin-top: 10px;
+       padding: 0 13px;
+       border-radius: 3px;
+}
+.modal-reason-title{
+       margin-top: 12px;
+    margin-bottom: 10px;
+    padding-top: 10px;
+    padding-bottom: 10px;
+}
+#TB_title {
+       background: #fcfcfc;
+       border-bottom: 1px solid #ddd;
+       height: 29px;
+       padding: 13px 0;
+}
+div#TB_ajaxWindowTitle {
+       font-size: 1.3em;
+}
+.meta summary.feedback-summary{
+       font-size: 1.2em;
+       font-weight: bold;
+       padding: 7px 0;
+}
+.gp-content ul.feedback-reason-list {
+       list-style-type: none;
+       display: inline-block;
+       padding-left: .5em;
+}
+.feedback-reason-list li {
+       width: 50%;
+       float: left;
+       padding-top: 4px;
+}
+.feedback-reason-list li label {
+       font-size: 1.1em;
+       cursor: pointer;
+}
+.feedback-reason-list li input[type="checkbox"] {
+       margin-right: 5px;
+}
+.feedback-comment label, .gp-content h3.feedback-reason-title {
+       font-size: 1.1em;
+       font-weight: bold;
+       margin: 0;
+}
+.feedback-comment textarea {
+       height: 110px;
+       font-size: 1.1em;
+}
+.status-actions details {
+       margin-bottom: 5px;
+}
+
+.async-content #legend {
+       margin-top: 2em;
+       float: none;
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/css/discussion.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_htmlwpcontentpluginsgptranslationhelperscsstranslationhelperscss"></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/gp-translation-helpers/css/translation-helpers.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/gp-translation-helpers/css/translation-helpers.css                               (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/css/translation-helpers.css 2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,88 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+.editor td {
+       vertical-align: top;
+}
+#translations .translation-helpers {
+       min-width: 400px;
+       padding: 0;
+       border: 0;
+       border-top:1px solid #eee;
+}
+.translation-helpers h4 {
+       margin-bottom: 0.5em;
+       font-size: 1.1em;
+       padding: .25em .5em;
+}
+.helpers-tabs {
+       margin: 0px;
+       padding: 0px;
+       list-style: none;
+       border-bottom: 2px solid #ccc;
+       white-space: nowrap;
+}
+
+ul.helpers-tabs {
+       padding-left: 14px !important;
+}
+
+.helpers-tabs li {
+       background: #eee;
+       color: #222;
+       display: inline-block;
+       padding: 10px 15px;
+       cursor: pointer;
+       margin: 0 1px 0 0;
+       border-top: 1px solid #eee;
+}
+.helpers-tabs li.current {
+       background-color: transparent;
+       margin: 0 0 -2px -1px;
+       border: 1px solid #ccc;
+       border-bottom: 2px solid #f8ffec;
+       font-weight: bold;
+}
+.loading .helpers-tabs {
+       padding-right: 28px;
+       background: transparent url(https://s0.wp.com/wp-content/mu-plugins/notes/images/loading.gif) no-repeat right 4px center;
+       background-size: 20px;
+}
+.helper {
+       overflow-y: scroll;
+       max-height: 800px;
+       display: none;
+       padding: 1em;
+       min-height: 200px;
+}
+#original .helper {
+       overflow-y: initial;
+       max-height: fit-content;
+}
+.helper.current {
+       display: block;
+}
+.helpers-tabs li .count {
+       display: inline-block;
+       padding-left: 4px;
+       opacity: 0.6;
+}
+
+#translation-history-table {
+       width: 100%;
+       border-collapse: collapse;
+}
+
+#translation-history-table thead tr th {
+       font-size: 1rem;
+       font-weight: 900 !important;
+       padding: 10px;
+       background: #f9f9f9;
+       border: 1px solid rgb(0 0 0 / 5%);
+}
+
+#translation-history-table tbody tr td {
+       padding: 10px;
+       border: 1px solid rgb(0 0 0 / 5%);
+}
+
+footer em {
+       margin-left: 14px;
+}
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of file
</span><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/css/translation-helpers.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_htmlwpcontentpluginsgptranslationhelpersgptranslationhelpersphp"></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/gp-translation-helpers/gp-translation-helpers.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/gp-translation-helpers/gp-translation-helpers.php                                (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/gp-translation-helpers.php  2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,39 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * @package gp-translation-helpers
+ */
+
+/**
+ * Plugin name:     GP Translation Helpers
+ * Plugin URI:      https://github.com/GlotPress/gp-translation-helpers
+ * Description:     GlotPress plugin to discuss the strings that are being translated in GlotPress.
+ * Version:         0.0.2
+ * Requires PHP:    7.4
+ * Author:          the GlotPress team
+ * Author URI:      https://glotpress.blog
+ * License:         GPLv2 or later
+ * Text Domain:     gp-translation-helpers
+ *
+ * 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.
+ */
+
+require_once __DIR__ . '/includes/class-gp-route-translation-helpers.php';
+require_once __DIR__ . '/includes/class-gp-translation-helpers.php';
+require_once __DIR__ . '/includes/class-gth-temporary-post.php';
+require_once __DIR__ . '/includes/class-gp-notifications.php';
+require_once __DIR__ . '/includes/class-wporg-notifications.php';
+
+add_action( 'gp_init', array( 'GP_Translation_Helpers', 'init' ) );
+add_action( 'gp_init', array( 'WPorg_GlotPress_Notifications', 'init' ) );    // todo: include this class in a different plugin.
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/gp-translation-helpers.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_htmlwpcontentpluginsgptranslationhelpershelpersbasehelperphp"></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/gp-translation-helpers/helpers/base-helper.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/gp-translation-helpers/helpers/base-helper.php                           (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers/base-helper.php     2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,268 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Base class, extended by all other helpers
+ *
+ * @package gp-translation-helpers
+ * @since 0.0.1
+ */
+class GP_Translation_Helper {
+
+       /**
+        * The folder where the assets are stored.
+        *
+        * @since 0.0.1
+        * @var string
+        */
+       public $assets_dir;
+
+       /**
+        * The data coming from the route.
+        *
+        * @since 0.0.1
+        * @var array
+        */
+       public $data;
+
+       /**
+        * GP_Translation_Helper constructor.
+        *
+        * Will throw a LogicException if the title property is not set.
+        *
+        * @since 0.0.1
+        *
+        * @throws LogicException If the class has not a must-have property.
+        */
+       final public function __construct() {
+               $this->assets_dir = dirname( dirname( __FILE__ ) ) . '/helpers-assets/';
+
+               $required_properties = array(
+                       'title',
+               );
+
+               foreach ( $required_properties as $prop ) {
+                       if ( ! isset( $this->{$prop} ) ) {
+                               throw new LogicException( get_class( $this ) . ' must have a property ' . $prop );
+                       }
+               }
+
+               if ( method_exists( $this, 'after_constructor' ) ) {
+                       $this->after_constructor();
+               }
+       }
+
+       /**
+        * Sets the data coming from the route.
+        *
+        * Sets values like project_id, locale_slug, set_slug, original_id, translation_id, etc.
+        *
+        * @since 0.0.1
+        *
+        * @param array $args   Data coming from the route.
+        */
+       public function set_data( array $args ) {
+               $this->data = $args;
+       }
+
+       /**
+        * Gets the priority of a helper. Defaults to 1 if not set.
+        *
+        * @since 0.0.1
+        *
+        * @return int
+        */
+       public function get_priority(): int {
+               return $this->priority ?? 1;
+       }
+
+       /**
+        * Indicates whether the helper loads asynchronous content or not.
+        *
+        * Defaults to false, but uses the class property if set.
+        *
+        * @since 0.0.1
+        *
+        * @return bool
+        */
+       public function has_async_content(): bool {
+               return $this->has_async_content ?? false;
+       }
+       /**
+        * Indicates whether the helper should initally load inline
+        *
+        * @since 0.0.2
+        *
+        * @return bool
+        */
+       public function load_inline(): bool {
+               return $this->load_inline ?? false;
+       }
+
+       /**
+        * Gets the class name for the helper div.
+        *
+        * @since 0.0.1
+        *
+        * @return string
+        */
+       public function get_div_classname(): string {
+               if ( isset( $this->classname ) ) {
+                       return $this->classname;
+               }
+
+               return sanitize_html_class( str_replace( '_', '-', strtolower( get_class( $this ) ) ), 'default-translation-helper' );
+       }
+
+       /**
+        * Gets the HTML id for the div.
+        *
+        * @since 0.0.1
+        *
+        * @return string
+        */
+       public function get_div_id(): string {
+               $div_id = $this->get_div_classname() . '-' . $this->data['original_id'];
+
+               if ( isset( $this->data['translation_id'] ) ) {
+                       $div_id .= '-' . $this->data['translation_id'];
+               } elseif ( isset( $this->data['translation'] ) ) {
+                       $div_id .= '-' . $this->data['translation']->id;
+               }
+
+               return $div_id;
+       }
+
+       /**
+        * Returns the title of the helper.
+        *
+        * @since 0.0.1
+        *
+        * @return string
+        */
+       public function get_title(): string {
+               return $this->title;
+       }
+
+       /**
+        * Indicates whether the helper should be active or not.
+        *
+        * Overwrite in the inheriting class to make this vary depending on class args.
+        *
+        * @since 0.0.1
+        *
+        * @return bool
+        */
+       public function activate() {
+               return true;
+       }
+
+       /**
+        * Sets the count of items returned by the helper.
+        *
+        * @since 0.0.1
+        *
+        * @param mixed $list   Elements to count.
+        */
+       public function set_count( $list ) {
+               if ( is_array( $list ) ) {
+                       $this->count = count( $list );
+               } else {
+                       $this->count = $list ? 1 : 0;
+               }
+       }
+
+       /**
+        * Gets the number of items returned by the helper.
+        *
+        * @since 0.0.1
+        *
+        * @return int
+        */
+       public function get_count(): int {
+               return $this->count ?? 0;
+       }
+
+       /**
+        * Gets the content/string to return when a helper has no results.
+        *
+        * @since 0.0.1
+        *
+        * @return string
+        */
+       public function empty_content() {
+               return __( 'No results found.' );
+       }
+
+       /**
+        * Default callback to render items returned by the helper.
+        *
+        * Gets an unordered list of the items that will be rendered by the helper.
+        *
+        * @since 0.0.1
+        *
+        * @param array $items  Elements to be grouped in an unordered list.
+        *
+        * @return string
+        */
+       public function async_output_callback( array $items ) {
+               $output = '<ul>';
+               foreach ( $items as $item ) {
+                       $output .= '<li>' . $item . '</li>';
+               }
+               $output .= '</ul>';
+               return $output;
+       }
+
+       /**
+        * Gets content that is returned asynchronously.
+        *
+        * @since 0.0.1
+        *
+        * @return string
+        */
+       public function get_async_output(): string {
+               $items = $this->get_async_content();
+               $this->set_count( $items );
+
+               if ( ! $items ) {
+                       return $this->empty_content();
+               }
+
+               return $this->async_output_callback( $items );
+       }
+
+       /**
+        * Gets the (non-async) output for the helper.
+        *
+        * @since 0.0.1
+        *
+        * @return string
+        */
+       public function get_output(): string {
+               if ( ! $this->load_inline() ) {
+                       return '<div class="loading">Loading&hellip;</div>';
+               }
+               return $this->async_output_callback( $this->get_async_content() );
+       }
+
+       /**
+        * Gets additional CSS required by the helper.
+        *
+        * @since 0.0.1
+        *
+        * @return bool|string
+        */
+       public function get_css() {
+               return false;
+       }
+
+       /**
+        * Gets additional JavaScript required by the helper.
+        *
+        * @since 0.0.1
+        *
+        * @return bool|string
+        */
+       public function get_js() {
+               return false;
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers/base-helper.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_htmlwpcontentpluginsgptranslationhelpershelpershelperotherlocalesphp"></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/gp-translation-helpers/helpers/helper-other-locales.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/gp-translation-helpers/helpers/helper-other-locales.php                          (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers/helper-other-locales.php    2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,186 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Helper showing translations from other locales
+ *
+ * @package gp-translation-helpers
+ * @since 0.0.1
+ */
+class Helper_Other_Locales extends GP_Translation_Helper {
+
+       /**
+        * Helper priority.
+        *
+        * @since 0.0.1
+        * @var int
+        */
+       public $priority = 3;
+
+       /**
+        * Helper title.
+        *
+        * @since 0.0.1
+        * @var string
+        */
+       public $title = 'Other locales';
+
+       /**
+        * Indicates whether the helper loads asynchronous content or not.
+        *
+        * @since 0.0.1
+        * @var bool
+        */
+       public $has_async_content = true;
+
+       /**
+        * Activates the helper.
+        *
+        * @since 0.0.2
+        *
+        * @return bool
+        */
+       public function activate(): bool {
+               if ( ! $this->data['project'] ) {
+                       return false;
+               }
+
+               if ( ! isset( $this->data['translation_set_slug'] ) || ! isset( $this->data['locale_slug'] ) ) {
+                       $this->title = 'Translations';
+               }
+
+               return true;
+       }
+
+       /**
+        * Gets content that is returned asynchronously.
+        *
+        * @since 0.0.1
+        *
+        * @return array|void
+        */
+       public function get_async_content() {
+               if ( ! $this->data['project'] ) {
+                       return;
+               }
+               $translation_set = null;
+               if ( isset( $this->data['translation_set_slug'] ) && isset( $this->data['locale_slug'] ) ) {
+                       $translation_set = GP::$translation_set->by_project_id_slug_and_locale( $this->data['project_id'], $this->data['translation_set_slug'], $this->data['locale_slug'] );
+               }
+
+               $translations           = GP::$translation->find_many_no_map(
+                       array(
+                               'status'      => 'current',
+                               'original_id' => $this->data['original_id'],
+                       )
+               );
+               $translations_by_locale = array();
+               foreach ( $translations as $translation ) {
+                       $_set = GP::$translation_set->get( $translation->translation_set_id );
+                       if ( ! $_set || ( $translation_set && intval( $translation->translation_set_id ) === intval( $translation_set->id ) ) ) {
+                               continue;
+                       }
+                       $translations_by_locale[ $_set->locale ] = $translation;
+               }
+
+               ksort( $translations_by_locale );
+
+               return $translations_by_locale;
+       }
+
+       /**
+        * Gets the items that will be rendered by the helper.
+        *
+        * @since 0.0.1
+        *
+        * @param array $translations   Translation history.
+        *
+        * @return string
+        */
+       public function async_output_callback( array $translations ): string {
+               $output  = '<ul class="other-locales">';
+               $project = null;
+               foreach ( $translations as $locale => $translation ) {
+                       $translation_set = GP::$translation_set->get( $translation->translation_set_id );
+                       if ( is_null( $project ) ) {
+                               $project = GP::$project->get( $translation_set->project_id );
+                       }
+
+                       $translation_permalink = GP_Route_Translation_Helpers::get_translation_permalink(
+                               $project,
+                               $translation_set->locale,
+                               $translation_set->slug,
+                               $translation->original_id,
+                               $translation->id
+                       );
+
+                       if ( ( null === $translation->translation_1 ) && ( null === $translation->translation_2 ) &&
+                                ( null === $translation->translation_3 ) && ( null === $translation->translation_4 ) &&
+                                ( null === $translation->translation_5 ) ) {
+                               $output .= sprintf( '<li><span class="locale unique">%s</span>%s</li>', $locale, gp_link_get( $translation_permalink, esc_translation( $translation->translation_0 ) ) );
+                       } else {
+                               $output .= sprintf( '<li><span class="locale">%s</span>', $locale );
+                               $output .= '<ul>';
+                               for ( $i = 0; $i <= 5; $i ++ ) {
+                                       if ( null !== $translation->{'translation_' . $i} ) {
+                                               $output .= sprintf( '<li>%s</li>', gp_link_get( $translation_permalink, esc_translation( $translation->{'translation_' . $i} ) ) );
+                                       }
+                               }
+                               $output .= '</ul>';
+                               $output .= '</li>';
+                       }
+               }
+                       $output .= '</ul>';
+                       return $output;
+       }
+
+       /**
+        * Gets the text to display when no other locales have translated this string yet.
+        *
+        * @since 0.0.1
+        *
+        * @return string
+        */
+       public function empty_content(): string {
+               return esc_html__( 'No other locales have translated this string yet.' );
+       }
+
+       /**
+        * Gets the CSS for this helper.
+        *
+        * @since 0.0.1
+        *
+        * @return string
+        */
+       public function get_css(): string {
+               return <<<CSS
+       .other-locales {
+               list-style: none;
+       }
+       ul.other-locales {
+               padding-left: 0;
+       }
+       .other-locales li {
+               clear:both;
+       }
+       ul.other-locales li {
+               display: flex;
+       }
+       ul.other-locales li ul li {
+               display: list-item;
+               list-style: disc;
+       }
+       span.locale.unique {
+               margin-right: 26px;
+       }
+       .other-locales .locale {
+               display: inline-block;
+               padding: 1px 6px 0 0;
+               margin: 1px 6px 1px 0;
+               background: #00DA12;
+               width: 5em;
+               text-align: right;
+               float: left;
+               color: #fff;
+       }
+CSS;
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers/helper-other-locales.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_htmlwpcontentpluginsgptranslationhelpershelpershelpertranslationdiscussionphp"></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/gp-translation-helpers/helpers/helper-translation-discussion.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/gp-translation-helpers/helpers/helper-translation-discussion.php                         (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers/helper-translation-discussion.php   2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,957 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Helper that manages and shows the discussions
+ *
+ * @package gp-translation-helpers
+ * @since 0.0.1
+ */
+class Helper_Translation_Discussion extends GP_Translation_Helper {
+
+       /**
+        * Helper priority.
+        *
+        * @since 0.0.1
+        * @var int
+        */
+       public $priority = 0;
+
+       /**
+        * Helper title.
+        *
+        * @since 0.0.1
+        * @var string
+        */
+       public $title = 'Discussion';
+
+       /**
+        * Indicates whether the helper loads asynchronous content or not.
+        *
+        * @since 0.0.1
+        * @var bool
+        */
+       public $has_async_content = true;
+
+       /**
+        * Indicates whether the helper should be loaded inline.
+        *
+        * @since 0.0.2
+        * @var bool
+        */
+       public $load_inline = true;
+
+       /**
+        * The post type used to store the comments.
+        *
+        * @since 0.0.1
+        * @var string
+        */
+       const POST_TYPE = 'gth_original';
+
+       /**
+        * The comment post status. Creates it as published.
+        *
+        * @since 0.0.1
+        * @var string
+        */
+       const POST_STATUS = 'publish';
+
+       /**
+        * The taxonomy key.
+        *
+        * @since 0.0.1
+        * @var string
+        */
+       const LINK_TAXONOMY = 'gp_original_id';
+
+       /**
+        *
+        * @since 0.0.1
+        * @var string
+        */
+       const URL_SLUG = 'discuss';
+
+       /**
+        *
+        * @since 0.0.1
+        * @var string
+        */
+       const ORIGINAL_ID_PREFIX = 'original-';
+
+       /**
+        * Registers the post type, its taxonomy, the comments' metadata and adds a filter to moderate the comments.
+        *
+        * Method executed just after the constructor.
+        *
+        * @since 0.0.1
+        *
+        * @return void
+        */
+       public function after_constructor() {
+               $this->register_post_type_and_taxonomy();
+               add_filter( 'pre_comment_approved', array( $this, 'comment_moderation' ), 10, 2 );
+               add_filter( 'map_meta_cap', array( $this, 'map_comment_meta_caps' ), 10, 4 );
+               add_filter( 'user_has_cap', array( $this, 'give_user_read_cap' ), 10, 3 );
+               add_filter( 'post_type_link', array( $this, 'rewrite_original_post_type_permalink' ), 10, 2 );
+               add_filter( 'comment_reply_link', array( $this, 'comment_reply_link' ), 10, 4 );
+               add_filter( 'wp_ajax_create_shadow_post', array( $this, 'ajax_create_shadow_post' ) );
+       }
+
+       /**
+        * Registers the post type with its taxonomy and the comments' metadata.
+        *
+        * @since 0.0.1
+        *
+        * @return void
+        */
+       public function register_post_type_and_taxonomy() {
+               register_taxonomy(
+                       self::LINK_TAXONOMY,
+                       array(),
+                       array(
+                               'public'       => false,
+                               'show_ui'      => false,
+                               'rewrite'      => false,
+                               'capabilities' => array(
+                                       'assign_terms' => 'read',
+                               ),
+                       )
+               );
+
+               $post_type_args = array(
+                       'supports'          => array( 'comments' ),
+                       'show_ui'           => false,
+                       'show_in_menu'      => false,
+                       'show_in_admin_bar' => false,
+                       'show_in_nav_menus' => false,
+                       'can_export'        => false,
+                       'has_archive'       => false,
+                       'show_in_rest'      => true,
+                       'taxonomies'        => array( self::LINK_TAXONOMY ),
+                       'rewrite'           => false,
+               );
+
+               register_post_type( self::POST_TYPE, $post_type_args );
+
+               register_meta(
+                       'comment',
+                       'translation_id',
+                       array(
+                               'description'       => 'Translation that was displayed when the comment was posted',
+                               'single'            => true,
+                               'show_in_rest'      => true,
+                               'sanitize_callback' => array( $this, 'sanitize_translation_id' ),
+                       )
+               );
+
+               register_meta(
+                       'comment',
+                       'locale',
+                       array(
+                               'description'       => 'Locale slug associated with a string comment',
+                               'single'            => true,
+                               'show_in_rest'      => true,
+                               'sanitize_callback' => array( $this, 'sanitize_comment_locale' ),
+                               'rewrite'           => false,
+                       )
+               );
+
+               register_meta(
+                       'comment',
+                       'comment_topic',
+                       array(
+                               'description'       => 'Reason for the comment',
+                               'single'            => true,
+                               'show_in_rest'      => true,
+                               'sanitize_callback' => array( $this, 'sanitize_comment_topic' ),
+                               'rewrite'           => false,
+                       )
+               );
+       }
+
+       /**
+        * Give subscribers permission to add our comment metas.
+        *
+        * @param      array  $caps     The capabilities they need to have.
+        * @param      string $cap      The capability we're testing for.
+        * @param      int    $user_id  The user id.
+        * @param      array  $args     Other arguments.
+        *
+        * @return     array  The capabilities they need to have.
+        */
+       public function map_comment_meta_caps( $caps, $cap, $user_id, $args ) {
+               if ( 'edit_comment_meta' === $cap && isset( $args[1] ) && in_array( $args[1], array( 'translation_id', 'locale', 'comment_topic' ), true ) ) {
+                       return array( 'read' );
+               }
+               return $caps;
+       }
+
+       /**
+        * Ensure that a user has the read capability on translate.wordpress.org.
+        *
+        * @param      array $allcaps  All capabilities of the uer.
+        * @param      array $caps     The capabilities requested.
+        * @param      array $args     Other arguments.
+        *
+        * @return     array  Potentially modified capabilities of the user.
+        */
+       public function give_user_read_cap( $allcaps, $caps, $args ) {
+               if ( ! defined( 'WPORG_TRANSLATE_BLOGID' ) || get_current_blog_id() !== WPORG_TRANSLATE_BLOGID ) {
+                       return $allcaps;
+               }
+
+               if ( in_array( 'read', $caps, true ) && is_user_logged_in() && ! is_admin() ) {
+                       $allcaps['read'] = true;
+               }
+
+               return $allcaps;
+       }
+
+       /**
+        * Gets the permalink and stores in the cache.
+        *
+        * @since 0.0.2
+        *
+        * @param string  $post_link The post's permalink.
+        * @param WP_Post $post      The post in question.
+        *
+        * @return mixed|string
+        */
+       public function rewrite_original_post_type_permalink( string $post_link, WP_Post $post ) {
+               static $cache = array();
+
+               if ( self::POST_TYPE !== $post->post_type ) {
+                       return $post_link;
+               }
+
+               if ( isset( $cache[ $post->ID ] ) ) {
+                       return $cache[ $post->ID ];
+               }
+
+               // Cache the error case and overwrite it later if we succeed.
+               $cache[ $post->ID ] = $post_link;
+
+               $original_id = self::get_original_from_post_id( $post->ID );
+               if ( ! $original_id ) {
+                       return $cache[ $post->ID ];
+               }
+
+               $original = GP::$original->get( $original_id );
+               if ( ! $original ) {
+                       return $cache[ $post->ID ];
+               }
+
+               $project = GP::$project->get( $original->project_id );
+               if ( ! $project ) {
+                       return $cache[ $post->ID ];
+               }
+
+               // We were able to gather all information, let's put it in the cache.
+               $cache[ $post->ID ] = GP_Route_Translation_Helpers::get_permalink( $project->path, $original_id );
+
+               return $cache[ $post->ID ];
+       }
+
+       /**
+        * Updates the comment's approval status before it is set.
+        *
+        * It only updates the approved status if the user has previous translations.
+        *
+        * @since 0.0.1
+        *
+        * @param int|string|WP_Error $approved    The approval status. Accepts 1, 0, 'spam', 'trash',
+        *                                         or WP_Error.
+        * @param array               $commentdata Comment data.
+        *
+        * @return bool|int|string|WP_Error|null
+        */
+       public function comment_moderation( $approved, array $commentdata ) {
+               global $wpdb;
+
+               // If the comment is already approved, we're good.
+               if ( $approved ) {
+                       return $approved;
+               }
+
+               // We only care on comments on our specific post type.
+               if ( self::POST_TYPE !== get_post_type( $commentdata['comment_post_ID'] ) ) {
+                       return $approved;
+               }
+
+               // We can't do much if the comment was posted logged out.
+               if ( empty( $commentdata['comment_author'] ) ) {
+                       return $approved;
+               }
+
+               // If our user has already contributed translations, approve comment.
+               $user_current_translations = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->gp_translations WHERE user_id = %s AND status = 'current'", $commentdata['comment_author'] ) );
+               if ( $user_current_translations ) {
+                       $approved = true;
+               }
+
+               return $approved;
+       }
+
+       /**
+        * Gets the slug for the post ID.
+        *
+        * @since 0.0.1
+        *
+        * @param $post_id  The post ID.
+        *
+        * @return false|string
+        */
+       public static function get_original_from_post_id( $post_id ) {
+               if ( self::is_temporary_post_id( $post_id ) ) {
+                       return self::get_original_id_from_temporary_post_id( $post_id );
+               }
+
+               $terms = wp_get_object_terms( $post_id, self::LINK_TAXONOMY, array( 'number' => 1 ) );
+               if ( empty( $terms ) ) {
+                       return false;
+               }
+
+               return $terms[0]->slug;
+       }
+
+       /**
+        * Indicates whether the post id is a real one or a temporary one.
+        *
+        * @since 0.0.2
+        *
+        * @param int|string $post_id The post ID.
+        *
+        * @return bool
+        */
+       public static function is_temporary_post_id( $post_id ) {
+               return self::get_original_id_from_temporary_post_id( $post_id ) > 0;
+       }
+
+       /**
+        * Extract the original_id from the temporary post_id.
+        *
+        * @param      int|string $post_id The post ID.
+        *
+        * @return     int|null The original_id or null if the post_id is not a temporary one.
+        */
+       public static function get_original_id_from_temporary_post_id( $post_id ) {
+               if ( self::POST_TYPE !== substr( $post_id, 0, strlen( self::POST_TYPE ) ) ) {
+                       return null;
+               }
+               $original_id = substr( $post_id, strlen( self::POST_TYPE ) );
+               if ( is_numeric( $original_id ) && $original_id > 0 ) {
+                       return intval( $original_id );
+               }
+
+               return null;
+       }
+
+       /**
+        * Gets the post id of the shadow post and ensure its or create shadow post identifier.
+        *
+        * @param      int $original_id  The original identifier
+        *
+        * @return     <type>  The or create shadow post identifier.
+        */
+       public static function get_or_create_shadow_post_id( int $original_id ) {
+               return self::get_shadow_post_id( $original_id, true );
+       }
+
+       /**
+        * Get a Gth_Temporary_Post or a WP_Post, depending on the given post_id.
+        *
+        * @param      int|string $post_id  The post ID (could be a temporary one).
+        *
+        * @return     WP_Post|Gth_Temporary_Post  An object for use in wp_list_comments.
+        */
+       public static function maybe_get_temporary_post( $post_id ) {
+               if ( self::is_temporary_post_id( $post_id ) ) {
+                       return new Gth_Temporary_Post( $post_id );
+               }
+
+               return get_post( $post_id );
+       }
+
+       /**
+        * Gets the post id for the comments and stores it in the cache.
+        *
+        * @since 0.0.1
+        *
+        * @param int  $original_id  The original id for the string to translate. E.g. "2440".
+        * @param bool $create       Whether to create a post if it doesn't exist.
+        *
+        * @return int|WP_Error
+        */
+       public static function get_shadow_post_id( int $original_id, $create = false ) {
+               $cache_key = self::LINK_TAXONOMY . '_' . $original_id;
+
+               $post_id = wp_cache_get( $cache_key );
+               if ( false !== $post_id ) {
+                       // Something was found in the cache.
+
+                       if ( self::is_temporary_post_id( $post_id ) && $create ) {
+                               // a fake post_id was stored in the cache but we need to create an entry.
+                               // Let's pretend a cache fail, so that we get a chance to create an entry unless one already exists.
+                               $post_id = false;
+                       }
+               }
+
+               if ( 'production' !== wp_get_environment_type() || false === $post_id ) {
+                       $post_id  = null;
+                       $gp_posts = get_posts(
+                               array(
+                                       'tax_query'        => array(
+                                               array(
+                                                       'taxonomy' => self::LINK_TAXONOMY,
+                                                       'terms'    => $original_id,
+                                                       'field'    => 'slug',
+                                               ),
+                                       ),
+                                       'post_type'        => self::POST_TYPE,
+                                       'posts_per_page'   => 1,
+                                       'post_status'      => self::POST_STATUS,
+                                       'suppress_filters' => false,
+                               )
+                       );
+
+                       if ( ! empty( $gp_posts ) ) {
+                               $post_id = $gp_posts[0]->ID;
+                       } elseif ( $create ) {
+                               $post_id = wp_insert_post(
+                                       array(
+                                               'post_type'      => self::POST_TYPE,
+                                               'tax_input'      => array(
+                                                       self::LINK_TAXONOMY => array( strval( $original_id ) ),
+                                               ),
+                                               'post_status'    => self::POST_STATUS,
+                                               'post_author'    => 0,
+                                               'comment_status' => 'open',
+                                       )
+                               );
+                       } else {
+                               $post_id = self::POST_TYPE . strval( $original_id );
+                       }
+               }
+
+               wp_cache_add( $cache_key, $post_id );
+               return $post_id;
+       }
+
+       /**
+        * Gets the comments for the post.
+        *
+        * @since 0.0.1
+        *
+        * @return array
+        */
+       public function get_async_content(): array {
+               $post_id = self::get_shadow_post_id( $this->data['original_id'] );
+               if ( self::is_temporary_post_id( $post_id ) ) {
+                       return array();
+               }
+
+               return get_comments(
+                       array(
+                               'post_id'            => $post_id,
+                               'status'             => 'approve',
+                               'type'               => 'comment',
+                               'include_unapproved' => array( get_current_user_id() ),
+                       )
+               );
+       }
+
+       /**
+        * Shows the discussion template with the comment form.
+        *
+        * @since 0.0.1
+        *
+        * @param array $comments   The comments to display.
+        *
+        * @return false|string
+        */
+       public function async_output_callback( array $comments ) {
+               $this->set_count( $comments );
+
+               // Remove comment likes for now (or forever :) ).
+               remove_filter( 'comment_text', 'comment_like_button', 12 );
+
+               // Disable subscribe to posts.
+               add_filter( 'option_stb_enabled', '__return_false' );
+
+               // Disable subscribe to comments for now.
+               add_filter( 'option_stc_disabled', '__return_true' );
+
+               // Link comment author to their profile.
+               add_filter(
+                       'get_comment_author_link',
+                       function( $return, $author, $comment_id ) {
+                               $comment = get_comment( $comment_id );
+                               if ( ! empty( $comment->user_id ) ) {
+                                       $user = get_userdata( $comment->user_id );
+                                       if ( $user ) {
+                                               return gp_link_user( $user );
+                                       }
+                               }
+                               return $return;
+                       },
+                       10,
+                       3
+               );
+
+               add_filter(
+                       'comment_form_logged_in',
+                       function( $logged_in_as, $commenter, $user_identity ) {
+                               /* translators: Username with which the user is logged in */
+                               return sprintf( '<p class="logged-in-as">%s</p>', sprintf( __( 'Logged in as %s.' ), $user_identity ) );
+                       },
+                       10,
+                       3
+               );
+
+               add_filter(
+                       'comment_form_fields',
+                       function( $comment_fields ) {
+                               $comment_fields['comment'] = str_replace( '>Comment<', '>Please leave your comment about this string here:<', $comment_fields['comment'] );
+                               return $comment_fields;
+                       }
+               );
+
+               remove_action( 'comment_form_top', 'rosetta_comment_form_support_hint' );
+
+               $post = self::maybe_get_temporary_post( self::get_shadow_post_id( $this->data['original_id'] ) );
+
+               $output = gp_tmpl_get_output(
+                       'translation-discussion-comments',
+                       array(
+                               'comments'             => $comments,
+                               'post'                 => $post,
+                               'translation_id'       => isset( $this->data['translation_id'] ) ? $this->data['translation_id'] : null,
+                               'locale_slug'          => $this->data['locale_slug'],
+                               'original_permalink'   => $this->data['original_permalink'],
+                               'original_id'          => $this->data['original_id'],
+                               'project'              => $this->data['project'],
+                               'translation_set_slug' => $this->data['translation_set_slug'],
+
+                       ),
+                       $this->assets_dir . 'templates'
+               );
+               return $output;
+       }
+
+       /**
+        * Gets the content/string to return when a helper has no results.
+        *
+        * @since 0.0.1
+        *
+        * @return false|string
+        */
+       public function empty_content() {
+               return $this->async_output_callback( array() );
+       }
+
+       /**
+        * Gets additional CSS required by the helper.
+        *
+        * @since 0.0.1
+        *
+        * @return bool|string
+        */
+       public function get_css() {
+               return file_get_contents( $this->assets_dir . 'css/translation-discussion.css' );
+       }
+
+       /**
+        * Gets additional JavaScript required by the helper.
+        *
+        * @since 0.0.1
+        *
+        * @return bool|string
+        */
+       public function get_js() {
+               return file_get_contents( $this->assets_dir . 'js/translation-discussion.js' );
+       }
+
+       /**
+        * Sets the comment_topic meta_key as "unknown" if is not in the accepted values.
+        *
+        * Used as sanitize callback in the register_meta for the "comment" object type,
+        * 'comment_topic' meta_key
+        *
+        * @since 0.0.2
+        *
+        * @param string $comment_topic The meta_value for the meta_key "comment_topic".
+        *
+        * @return string
+        */
+       public function sanitize_comment_topic( string $comment_topic ): string {
+               if ( ! in_array( $comment_topic, array( 'typo', 'context', 'question' ), true ) ) {
+                       $comment_topic = 'unknown';
+               }
+               return $comment_topic;
+
+       }
+
+       /**
+        * Sets the comment_topic meta_key as empty ("") if is not in the accepted values.
+        *
+        * Used as sanitize callback in the register_meta for the "comment" object type,
+        * "locale" meta_key
+        *
+        * @since 0.0.2
+        *
+        * @param string $comment_locale     The meta_value for the meta_key "locale".
+        *
+        * @return string
+        */
+       public function sanitize_comment_locale( string $comment_locale ): string {
+               $gp_locales     = new GP_Locales();
+               $all_gp_locales = array_keys( $gp_locales->locales );
+
+               if ( ! in_array( $comment_locale, $all_gp_locales, true ) ) {
+                       $comment_locale = '';
+               }
+               return $comment_locale;
+       }
+
+       /**
+        * Throws an exception with an error message if the translation id is incorrect.
+        *
+        * Used as sanitize callback in the register_meta for the "comment" object type,
+        * "locale" meta_key
+        *
+        * The string type (input and output) is because it is the type if $translation_id is empty.
+        *
+        * @since 0.0.2
+        *
+        * @param int|string $translation_id   The id for the translation showed when the comment was made.
+        *
+        * @return int|string
+        *
+        * @throws Exception Throws an exception with message if translation_id is invalid.
+        */
+       public function sanitize_translation_id( $translation_id ) {
+               if ( $translation_id > 0 && ! GP::$translation->get( $translation_id ) ) {
+                       throw new Exception( 'Invalid translation ID' );
+               }
+               return $translation_id;
+       }
+
+       /**
+        * The comment reply link override.
+        *
+        * @param      string  $link     The link.
+        * @param      array   $args     The arguments.
+        * @param      string  $comment  The comment.
+        * @param      WP_Post $post     The post.
+        *
+        * @return     string  Return the reply link HTML.
+        */
+       public function comment_reply_link( $link, $args, $comment, $post ) {
+               $data_attributes = array(
+                       'commentid'      => $comment->comment_ID,
+                       'postid'         => $post->ID,
+                       'belowelement'   => $args['add_below'] . '-' . $comment->comment_ID,
+                       'respondelement' => $args['respond_id'],
+                       'replyto'        => sprintf( $args['reply_to_text'], $comment->comment_author ),
+               );
+
+               $data_attribute_string = '';
+
+               foreach ( $data_attributes as $name => $value ) {
+                       $data_attribute_string .= ' data-' . $name . '="' . esc_attr( $value ) . '"';
+               }
+
+               $data_attribute_string = trim( $data_attribute_string );
+
+               $link = sprintf(
+                       "<a rel='nofollow' class='comment-reply-link' href='%s' %s aria-label='%s'>%s</a>",
+                       esc_url(
+                               add_query_arg(
+                                       array(
+                                               'replytocom'      => $comment->comment_ID,
+                                               'unapproved'      => false,
+                                               'moderation-hash' => false,
+                                       ),
+                                       $args['original_permalink']
+                               )
+                       ) . '#' . $args['respond_id'],
+                       $data_attribute_string,
+                       esc_attr( sprintf( $args['reply_to_text'], $comment->comment_author ) ),
+                       $args['reply_text']
+               );
+               return $args['before'] . $link . $args['after'];
+       }
+
+       /**
+        * Ajax callback for creating the shadow post.
+        *
+        * Returns the $post_id back to JS.
+        *
+        * @since 0.0.2
+        */
+       public function ajax_create_shadow_post() {
+               check_ajax_referer( 'wp_rest', 'nonce' );
+
+               $original_id = self::get_original_id_from_temporary_post_id( $_POST['data']['post'] );
+               $post_id     = self::get_or_create_shadow_post_id( $original_id );
+               wp_send_json_success( $post_id );
+       }
+
+       /**
+        * Throws an exception with an error message if the original id is incorrect.
+        *
+        * Used as callback to validate the original_id passed on rejecting a string with feedback
+        *
+        * @since 0.0.2
+        *
+        * @param int|string $original_id   The id of the original for the rejected translation.
+        *
+        * @return int|string
+        *
+        * @throws Exception Throws an exception with message if original_id is invalid.
+        */
+       public function sanitize_original_id( $original_id ) {
+               if ( $original_id > 0 && ! GP::$original->get( $original_id ) ) {
+                       throw new Exception( 'Invalid Original ID' );
+               }
+
+               return $original_id;
+       }
+
+       /**
+        * Return an array of allowed rejection reasons
+        *
+        * @since 0.0.2
+        *
+        * @return array
+        */
+       public static function get_reject_reasons() {
+               return array(
+                       'style'       => __( 'Style Guide' ),
+                       'grammar'     => __( 'Grammar' ),
+                       'branding'    => __( 'Branding' ),
+                       'glossary'    => __( 'Glossary' ),
+                       'punctuation' => __( 'Punctuation' ),
+                       'typo'        => __( 'Typo' ),
+               );
+       }
+
+}
+
+/**
+ * Gets the slug for the post ID.
+ *
+ * @since 0.0.1
+ *
+ * @param int $post_id  The id of the post.
+ *
+ * @return false|string
+ */
+function gth_discussion_get_original_id_from_post( int $post_id ) {
+       return Helper_Translation_Discussion::get_original_from_post_id( $post_id );
+}
+
+/**
+ * Print a (linked) translation.
+ *
+ * @param      int    $comment_translation_id  The comment translation identifier.
+ * @param      array  $args                    The arguments.
+ * @param      string $prefix                  The prefix text.
+ */
+function gth_print_translation( $comment_translation_id, $args, $prefix = '' ) {
+       static $cache = array();
+       if ( ! isset( $cache[ $comment_translation_id ] ) ) {
+               $cache[ $comment_translation_id ] = GP::$translation->get( $comment_translation_id );
+       }
+       $translation           = $cache[ $comment_translation_id ];
+       $translation_permalink = GP_Route_Translation_Helpers::get_translation_permalink(
+               $args['project'],
+               $args['locale_slug'],
+               $args['translation_set_slug'],
+               $args['original_id'],
+               $comment_translation_id
+       );
+       ?>
+                       <em>
+                       <?php
+                       echo esc_html( $prefix );
+                       if ( $translation_permalink ) {
+                               echo wp_kses( gp_link( $translation_permalink, $translation->translation_0 ), array( 'a' => array( 'href' => true ) ) );
+                       } else {
+                               echo esc_html( $translation->translation_0 );
+                       }
+                       ?>
+                       </em>
+               <?php
+}
+/**
+ * Callback for the wp_list_comments() function in the helper-translation-discussion.php template.
+ *
+ * @since 0.0.1
+ *
+ * @param WP_Comment $comment   The comment object.
+ * @param array      $args      Formatting options.
+ * @param int        $depth     The depth of the new comment.
+ *
+ * @return void
+ */
+function gth_discussion_callback( WP_Comment $comment, array $args, int $depth ) {
+       $GLOBALS['comment'] = $comment;// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+
+       $is_linking_comment = preg_match( '!^' . home_url( gp_url() ) . '[a-z0-9_/#-]+$!i', $comment->comment_content );
+
+       $comment_locale = get_comment_meta( $comment->comment_ID, 'locale', true );
+       $current_locale = $args['locale_slug'];
+
+       $current_translation_id = $args['translation_id'];
+       $comment_translation_id = get_comment_meta( $comment->comment_ID, 'translation_id', true );
+
+       $reject_reason = get_comment_meta( $comment->comment_ID, 'reject_reason', true );
+
+       $classes = array( 'comment-locale-' . $comment_locale );
+       if ( ! empty( $reject_reason ) ) {
+               $classes[] = 'rejection-feedback';
+               $classes[] = 'rejection-feedback-' . $comment_locale;
+       }
+       ?>
+       <li class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>">
+       <article id="comment-<?php comment_ID(); ?>" class="comment">
+       <div class="comment-avatar">
+       <?php echo get_avatar( $comment, 25 ); ?>
+       </div><!-- .comment-avatar -->
+       <?php printf( '<cite class="fn">%s</cite>', get_comment_author_link( $comment->comment_ID ) ); ?>
+       <a href="<?php echo esc_url( get_comment_link( $comment->comment_ID ) ); ?>">
+       <?php
+       // Older than a week, show date; otherwise show __ time ago.
+       if ( time() - get_comment_time( 'U', true ) > 604800 ) {
+               /* translators: 1: Date , 2: Time */
+               $time = sprintf( _x( '%1$s at %2$s', '1: date, 2: time' ), get_comment_date(), get_comment_time() );
+       } else {
+               /* translators: Human readable time difference */
+               $time = sprintf( __( '%1$s ago' ), human_time_diff( get_comment_time( 'U' ), time() ) );
+       }
+       echo '<time datetime=" ' . get_comment_time( 'c' ) . '">' . esc_html( $time ) . '</time>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+       ?>
+       </a>
+       <?php if ( $comment_locale ) : ?>
+       <div class="comment-locale">Locale:
+                               <?php if ( ! $current_locale ) : ?>
+                                       <a href="<?php echo esc_attr( $comment_locale . '/default' ); ?>"><?php echo esc_html( $comment_locale ); ?></a>
+                               <?php elseif ( $current_locale && $current_locale !== $comment_locale ) : ?>
+                                       <a href="<?php echo esc_attr( '../../' . $comment_locale . '/default' ); ?>"><?php echo esc_html( $comment_locale ); ?></a>
+                               <?php else : ?>
+                                       <?php echo esc_html( $comment_locale ); ?>
+       <?php endif; ?>
+       </div>
+       <?php endif; ?>
+       <div class="comment-content" dir="auto">
+               <?php
+               if ( $is_linking_comment ) :
+                       $linked_comment = $comment->comment_content;
+                       $parts          = wp_parse_url( $linked_comment );
+                       $parts['path']  = rtrim( $parts['path'], '/' );
+                       $path_parts     = explode( '/', $parts['path'] );
+
+                       $linking_comment_set_slug = array_pop( $path_parts );
+                       $linking_comment_locale   = array_pop( $path_parts );
+                       if ( $current_locale && $current_locale !== $linking_comment_locale ) {
+                               $linked_comment = str_replace( $parts['path'], $parts['path'] . '/' . $current_locale . '/default', $linked_comment );
+                       }
+
+                       if ( $reject_reason ) :
+                               ?>
+                               The translation <?php gth_print_translation( $comment_translation_id, $args ); ?> was rejected with <a href="<?php echo esc_url( $linked_comment ); ?>"><?php esc_html_e( 'a reason that is being discussed here' ); ?></a>.
+                       <?php else : ?>
+                               <a href="<?php echo esc_url( $linked_comment ); ?>"><?php esc_html_e( 'Please continue the discussion here' ); ?></a>
+                       <?php endif; ?>
+               <?php else : ?>
+                       <?php comment_text(); ?>
+                       <?php if ( $reject_reason ) : ?>
+                       <p>
+                               <?php echo esc_html( _n( 'Rejection Reason: ', 'Rejection Reasons: ', count( $reject_reason ) ) ); ?>
+                               <span><?php echo wp_kses( implode( '</span> | <span>', $reject_reason ), array( 'span' => array() ) ); ?></span>
+                       </p>
+                       <?php endif; ?>
+               <?php endif; ?>
+       </div>
+       <footer>
+       <div class="comment-author vcard">
+                       <?php
+                       if ( $comment->comment_parent ) {
+                               printf(
+                                       '<a href="%1$s">%2$s</a>',
+                                       esc_url( get_comment_link( $comment->comment_parent ) ),
+                                       /* translators: The author of the current comment */
+                                       sprintf( esc_attr( __( 'in reply to %s' ) ), esc_html( get_comment_author( $comment->comment_parent ) ) )
+                               );
+                       }
+                       if ( $is_linking_comment ) {
+                               ?>
+                               <span class="alignright">
+                                       <a href="<?php echo esc_url( $comment->comment_content ); ?>"><?php esc_html_e( 'Reply' ); ?></a>
+                               </span>
+                               <?php
+                       } else {
+                               comment_reply_link(
+                                       array_merge(
+                                               $args,
+                                               array(
+                                                       'depth'     => $depth,
+                                                       'max_depth' => $args['max_depth'],
+                                                       'before'    => '<span class="alignright">',
+                                                       'after'     => '</span>',
+                                               )
+                                       )
+                               );
+                       }
+                       ?>
+                       </div><!-- .comment-author .vcard -->
+                       <?php
+                       if ( '0' === $comment->comment_approved ) {
+                               ?>
+                               <p><em><?php esc_html_e( 'Your comment is awaiting moderation.' ); ?></em></p>
+                               <?php
+                       }
+
+                       if ( ! $is_linking_comment ) :
+                               if ( $comment_translation_id && $comment_translation_id !== $current_translation_id ) {
+                                       gth_print_translation( $comment_translation_id, $args, empty( $reject_reason ) ? 'Translation: ' : 'Translation  (Rejected): ' );
+                               }
+
+                               ?>
+                               <div id="comment-reply-<?php echo esc_attr( $comment->comment_ID ); ?>" style="display: none;">
+                               <?php
+                               if ( is_user_logged_in() ) {
+                                       comment_form(
+                                               array(
+                                                       'title_reply'         => esc_html__( 'Discuss this string' ),
+                                                       /* translators: username */
+                                                       'title_reply_to'      => esc_html__( 'Reply to %s' ),
+                                                       'title_reply_before'  => '<h5 id="reply-title" class="discuss-title">',
+                                                       'title_reply_after'   => '</h5>',
+                                                       'id_form'             => 'commentform-' . $comment->comment_post_ID,
+                                                       'cancel_reply_link'   => '<span></span>',
+                                                       'comment_notes_after' => implode(
+                                                               "\n",
+                                                               array(
+                                                                       '<input type="hidden" name="comment_parent" value="' . esc_attr( $comment->comment_ID ) . '" />',
+                                                                       '<input type="hidden" name="comment_locale" value="' . esc_attr( $args['locale_slug'] ) . '" />',
+                                                                       '<input type="hidden" name="translation_id" value="' . esc_attr( $args['translation_id'] ) . '" />',
+                                                                       '<input type="hidden" name="redirect_to" value="' . esc_url( $args['original_permalink'] ) . '" />',
+                                                               )
+                                                       ),
+                                               ),
+                                               $comment->comment_post_ID
+                                       );
+                               } else {
+                                       /* translators: Log in URL. */
+                                       echo sprintf( __( 'You have to be <a href="%s">logged in</a> to comment.' ), esc_html( wp_login_url() ) );
+                               }
+                               ?>
+                       </div>
+                       <?php endif; ?>
+               </footer>
+       </article><!-- #comment-## -->
+</li>
+       <?php
+}
+
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers/helper-translation-discussion.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_htmlwpcontentpluginsgptranslationhelpershelpershelpertranslationhistoryphp"></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/gp-translation-helpers/helpers/helper-translation-history.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/gp-translation-helpers/helpers/helper-translation-history.php                            (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers/helper-translation-history.php      2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,203 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Helper that shows the history for a string in the current locale
+ *
+ * @package gp-translation-helpers
+ * @since 0.0.1
+ */
+class Helper_History extends GP_Translation_Helper {
+
+       /**
+        * Helper priority.
+        *
+        * @since 0.0.1
+        * @var int
+        */
+       public $priority = 2;
+
+       /**
+        * Helper title.
+        *
+        * @since 0.0.1
+        * @var string
+        */
+       public $title = 'History';
+
+       /**
+        * Indicates whether the helper loads asynchronous content or not.
+        *
+        * @since 0.0.1
+        * @var bool
+        */
+       public $has_async_content = true;
+
+       /**
+        * Indicates whether the helper should be active or not.
+        *
+        * @since 0.0.2
+        *
+        * @return bool
+        */
+       public function activate(): bool {
+               if ( ! $this->data['translation_set_slug'] || ! isset( $this->data['translation_set_slug'] ) || ! isset( $this->data['locale_slug'] ) ) {
+                       // Deactivate when translation set is available.
+                       return false;
+               }
+
+               return true;
+       }
+
+       /**
+        * Gets asynchronously the translation history of the string.
+        *
+        * @since 0.0.1
+        *
+        * @return mixed|void
+        */
+       public function get_async_content() {
+               $translation_set = GP::$translation_set->by_project_id_slug_and_locale( $this->data['project_id'], $this->data['translation_set_slug'], $this->data['locale_slug'] );
+
+               if ( ! $translation_set ) {
+                       return;
+               }
+
+               $translations = GP::$translation->find_many_no_map(
+                       array(
+                               'translation_set_id' => $translation_set->id,
+                               'original_id'        => $this->data['original_id'],
+                       )
+               );
+
+               usort(
+                       $translations,
+                       function ( $t1, $t2 ) {
+                               $cmp_prop_t1 = $t1->date_modified ?? $t1->date_added;
+                               $cmp_prop_t2 = $t2->date_modified ?? $t2->date_added;
+                               return $cmp_prop_t1 < $cmp_prop_t2;
+                       }
+               );
+
+               $this->set_count( $translations );
+
+               return $translations;
+       }
+
+       /**
+        * Gets the items that will be rendered by the helper.
+        *
+        * @since 0.0.1
+        *
+        * @param array $translations   Translation history.
+        *
+        * @return string
+        */
+       public function async_output_callback( array $translations ): string {
+               if ( ! $translations ) {
+                       return '';
+               }
+               $output  = '<table id="translation-history-table" class="translations">';
+               $output .= '<thead>';
+               $output .= '<tr><th>Date</th><th>Translation</th><th>Added by</th><th>Last modified by</th>';
+               $output .= '</thead>';
+
+               foreach ( $translations as $key => $translation ) {
+                       $date_and_time = is_null( $translation->date_modified ) ? $translation->date_added : $translation->date_modified;
+                       $date_and_time = explode( ' ', $date_and_time );
+
+                       $user                  = get_userdata( $translation->user_id );
+                       $user_last_modified    = get_userdata( $translation->user_id_last_modified );
+                       $translation_permalink = GP_Route_Translation_Helpers::get_translation_permalink(
+                               $this->data['project'],
+                               $this->data['locale_slug'],
+                               $this->data['translation_set_slug'],
+                               $this->data['original_id'],
+                               $translation->id
+                       );
+
+                       if (
+                               is_null( $translation->translation_1 ) &&
+                               is_null( $translation->translation_2 ) &&
+                               is_null( $translation->translation_3 ) &&
+                               is_null( $translation->translation_4 ) &&
+                               is_null( $translation->translation_5 )
+                       ) {
+                                       $output_translation = $translation->translation_0;
+                       } else {
+                               $output_translation = '<ul>';
+                               for ( $i = 0; $i <= 5; $i ++ ) {
+                                       if ( null !== $translation->{'translation_' . $i} ) {
+                                               $output_translation .= sprintf( '<li>%s</li>', esc_translation( $translation->{'translation_' . $i} ) );
+                                       }
+                               }
+                               $output_translation .= '</ul>';
+                       }
+
+                       $output .= sprintf(
+                               '<tr class="preview status-%1$s"><td title="%2$s">%3$s</td><td>%4$s</td><td>%5$s</td><td>%6$s</td></tr>',
+                               esc_attr( $translation->status ),
+                               esc_attr( $translation->date_modified ?? $translation->date_added ),
+                               esc_html( $date_and_time[0] ),
+                               $translation_permalink ? '<a href="' . esc_url( $translation_permalink ) . '">' . esc_html( $output_translation ) . '</a>' : esc_html( $output_translation ),
+                               $user ? esc_html( $user->user_login ) : '&mdash;',
+                               $user_last_modified ? esc_html( $user_last_modified->user_login ) : '&mdash;'
+                       );
+               }
+
+               $output .= '</table>';
+
+               $output .= $this->get_translation_status_legend();
+
+               return $output;
+       }
+
+       /**
+        * Gets the translation status legend.
+        *
+        * @return     string  The translation status legend.
+        */
+       public function get_translation_status_legend() {
+               $legend  = '<div id="legend" class="secondary clearfix">';
+               $legend .= '<div><strong>' . esc_html__( 'Legend:', 'glotpress' ) . '</strong></div>';
+               foreach ( GP::$translation->get_static( 'statuses' ) as $legend_status ) {
+                       $legend .= '<div class="box status-' . esc_attr( $legend_status ) . '"></div>';
+                       $legend .= '<div>';
+                       switch ( $legend_status ) {
+                               case 'current':
+                                       $legend .= esc_html__( 'Current', 'glotpress' );
+                                       break;
+                               case 'waiting':
+                                       $legend .= esc_html__( 'Waiting', 'glotpress' );
+                                       break;
+                               case 'fuzzy':
+                                       $legend .= esc_html__( 'Fuzzy', 'glotpress' );
+                                       break;
+                               case 'old':
+                                       $legend .= esc_html__( 'Old', 'glotpress' );
+                                       break;
+                               case 'rejected':
+                                       $legend .= esc_html__( 'Rejected', 'glotpress' );
+                                       break;
+                               default:
+                                       $legend .= esc_html( $legend_status );
+                       }
+                       $legend .= '</div>';
+               }
+               $legend .= '<div class="box has-warnings"></div>';
+               $legend .= '<div>' . esc_html__( 'With warnings', 'glotpress' ) . '</div>';
+               $legend .= '</div>';
+               return $legend;
+       }
+
+       /**
+        * Gets the content/string to return when a helper has no results.
+        *
+        * @since 0.0.1
+        *
+        * @return string
+        */
+       public function empty_content(): string {
+               return esc_html__( 'No translation history for this string.' );
+       }
+
+
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers/helper-translation-history.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_htmlwpcontentpluginsgptranslationhelpershelpersassetscsstranslationdiscussioncss"></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/gp-translation-helpers/helpers-assets/css/translation-discussion.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/gp-translation-helpers/helpers-assets/css/translation-discussion.css                             (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers-assets/css/translation-discussion.css       2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,88 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+.helper-translation-discussion {
+       position:relative;
+       min-height: 600px;
+}
+.discussion-list {
+       list-style:none;
+       width: 100%;
+}
+
+ul.discussion-list {
+       display: contents;
+}
+
+ul.discussion-list li {
+       border-top: 1px solid rgb(0 0 0 / 5%);
+}
+
+ul.discussion-list li:first-child {
+       margin-top: 15px;
+}
+
+ul.discussion-list li:last-child {
+       border-bottom: 1px solid rgb(0 0 0 / 5%);
+       margin-bottom: 15px;
+}
+
+ul.discussion-list cite.fn {
+       margin-left: 12px;
+}
+
+ul.discussion-list .comment-avatar {
+       margin-left: -30px;
+}
+
+ul.discussion-list article.comment {
+       margin-bottom: 0;
+}
+
+ul.discussion-list ul.children {
+       list-style: none;
+}
+
+article.comment {
+       margin: 15px 30px 15px 30px;
+       padding-bottom: 15px;
+       position: relative;
+       font-size: 0.9rem;
+}
+article.comment p {
+       margin-bottom: 0.5em;
+}
+article.comment footer {
+       overflow: hidden;
+       font-size: 0.8rem;
+       font-style: normal;
+}
+article.comment time {
+       font-style: italic;
+       opacity: 0.8;
+       display: inline-block;
+       padding-left: 4px;
+}
+.comment-locale {
+       opacity: 0.8;
+       float: right;
+}
+.comment-avatar {
+       margin-left: -45px;
+       margin-bottom: -25px;
+       width: 50px;
+       height: 26px;
+}
+.comment-avatar img {
+       display: block;
+       border-radius: 13px;
+}
+.comments-selector {
+       display: inline-block;
+       padding-left: 10px;
+       font-size: 0.9em;
+}
+.comment-content {
+       text-align: start;
+}
+
+.alignright {
+       float: right;
+}
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of file
</span><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers-assets/css/translation-discussion.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_htmlwpcontentpluginsgptranslationhelpershelpersassetsjstranslationdiscussionjs"></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/gp-translation-helpers/helpers-assets/js/translation-discussion.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/gp-translation-helpers/helpers-assets/js/translation-discussion.js                               (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers-assets/js/translation-discussion.js 2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,104 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/* global $gp, document, wpApiSettings */
+jQuery( function( $ ) {
+       $( document ).on( 'click', '.helper-translation-discussion .comments-selector a', function( e ) {
+               var $comments, $selector;
+
+               e.preventDefault();
+               $( '.comments-selector a' ).removeClass( 'active-link' );
+               $( this ).addClass( 'active-link' );
+               $comments = jQuery( e.target ).parents( 'h6' ).next( '.discussion-list' );
+               $selector = $( e.target ).data( 'selector' );
+               if ( 'all' === $selector ) {
+                       $comments.children().show();
+               } else if ( 'rejection-feedback' === $selector ) {
+                       $comments.children().hide();
+                       $comments.children( '.rejection-feedback' ).show();
+               } else {
+                       $comments.children().hide();
+                       $comments.children( '.comment-locale-' + $selector ).show();
+                       $comments.children( '.comment-locale-' + $selector ).next( 'ul' ).show();
+               }
+               return false;
+       } );
+
+       function createShadowPost( formdata, submitComment ) {
+               var data = {
+                       action: 'create_shadow_post',
+                       data: formdata,
+                       _ajax_nonce: wpApiSettings.nonce,
+               };
+
+               $.ajax(
+                       {
+                               type: 'POST',
+                               url: wpApiSettings.admin_ajax_url,
+                               data: data,
+                       }
+               ).done(
+                       function( response ) {
+                               formdata.post = response.data;
+                               submitComment( formdata );
+                       }
+               );
+       }
+
+       $( document ).on( 'submit', '.helper-translation-discussion .comment-form', function( e ) {
+               var $commentform = $( e.target );
+               var postId = $commentform.attr( 'id' ).split( '-' )[ 1 ];
+               var submitComment = function( formdata ) {
+                       $.ajax( {
+                               url: wpApiSettings.root + 'wp/v2/comments',
+                               method: 'POST',
+                               beforeSend: function( xhr ) {
+                                       xhr.setRequestHeader( 'X-WP-Nonce', wpApiSettings.nonce );
+                               },
+                               data: formdata,
+                       } ).done( function( response ) {
+                               if ( 'undefined' !== typeof ( response.data ) ) {
+                                       // There's probably a better way, but response.data is only set for errors.
+                                       // TODO: error handling.
+                               } else {
+                                       $commentform.find( 'textarea[name=comment]' ).val( '' );
+                                       $gp.translation_helpers.fetch( 'discussion' );
+                               }
+                       } );
+               };
+
+               var formdata = {
+                       content: $commentform.find( 'textarea[name=comment]' ).val(),
+                       parent: $commentform.find( 'input[name=comment_parent]' ).val(),
+                       post: postId,
+                       meta: {
+                               translation_id: $commentform.find( 'input[name=translation_id]' ).val(),
+                               locale: $commentform.find( 'input[name=comment_locale]' ).val(),
+                               comment_topic: $commentform.find( 'select[name=comment_topic]' ).val(),
+                       },
+               };
+               e.preventDefault();
+               e.stopImmediatePropagation();
+
+               $( 'input.submit' ).prop( 'disabled', true );
+
+               if ( ! formdata.meta.translation_id ) {
+                       formdata.meta.translation_id = 0;
+               }
+
+               if ( formdata.meta.locale ) {
+                       /**
+                        * Set the locale to an empty string if option selected has value 'typo' or 'context'
+                        * to force comment to be posted to the English discussion page
+                        */
+                       if ( formdata.meta.comment_topic === 'typo' || formdata.meta.comment_topic === 'context' ) {
+                               formdata.meta.locale = '';
+                       }
+               }
+
+               if ( isNaN( Number( postId ) ) ) {
+                       createShadowPost( formdata, submitComment );
+               } else {
+                       submitComment( formdata );
+               }
+
+               return false;
+       } );
+} );
</ins></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpershelpersassetstemplatestranslationdiscussioncommentsphp"></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/gp-translation-helpers/helpers-assets/templates/translation-discussion-comments.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/gp-translation-helpers/helpers-assets/templates/translation-discussion-comments.php                              (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers-assets/templates/translation-discussion-comments.php        2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,123 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * The template part for the comments and comment form on an original discussion
+ */
+?>
+<div class="discussion-wrapper">
+       <?php if ( $comments ) : ?>
+
+               <h6>
+                       <?php
+                       /* translators: number of comments. */
+                       printf( _n( '%s Comment', '%s Comments', count( $comments ) ), number_format_i18n( count( $comments ) ) );
+                       ?>
+               <?php if ( $locale_slug ) : ?>
+                       (<?php echo esc_html( $locale_slug ); ?>)
+                       <?php
+                       $locale_comments_count    = 0;
+                       $count_rejection_feedback = 0;
+                       foreach ( $comments as $_comment ) {
+                               $comment_locale = get_comment_meta( $_comment->comment_ID, 'locale', true );
+                               if ( $locale_slug == $comment_locale ) {
+
+                                       $reject_reason = get_comment_meta( $_comment->comment_ID, 'reject_reason', true );
+                                       if ( ! empty( $reject_reason ) ) {
+                                               $count_rejection_feedback++;
+                                       }
+                                       $locale_comments_count++;
+                               }
+                       }
+                       ?>
+
+                       <span class="comments-selector">
+                               <a href="#" class="active-link" data-selector="all">Show all (<?php echo esc_html( count( $comments ) ); ?>)</a> |
+                               <a href="#" data-selector="<?php echo esc_attr( $locale_slug ); ?>"><?php echo esc_html( $locale_slug ); ?> only (<?php echo esc_html( $locale_comments_count ); ?>)</a> |
+                               <a href="#" data-selector="rejection-feedback">Rejection Feedback (<?php echo esc_html( $count_rejection_feedback ); ?>)</a>
+                       </span>
+               <?php endif; ?>
+               </h6>
+       <?php else : ?>
+               <?php esc_html_e( 'No comments have been made on this yet.' ); ?>
+       <?php endif; ?>
+       <ul class="discussion-list">
+               <?php
+               wp_list_comments(
+                       array(
+                               'style'                => 'ul',
+                               'type'                 => 'comment',
+                               'callback'             => 'gth_discussion_callback',
+                               'translation_id'       => $translation_id,
+                               'locale_slug'          => $locale_slug,
+                               'original_permalink'   => $original_permalink,
+                               'original_id'          => $original_id,
+                               'project'              => $project,
+                               'translation_set_slug' => $translation_set_slug,
+                               'reverse_children'     => false,
+                       ),
+                       $comments
+               );
+               ?>
+       </ul><!-- .discussion-list -->
+       <?php
+       add_action(
+               'comment_form_logged_in_after',
+               function () use ( $locale_slug ) {
+                       $language_question = '';
+
+                       if ( $locale_slug ) {
+                               $gp_locale = GP_Locales::by_slug( $locale_slug );
+                               if ( $gp_locale ) {
+                                       $language_question = '<option value="question">Question about translating to ' . esc_html( $gp_locale->english_name ) . '</option>';
+                               }
+                       }
+
+                       echo '<p class="comment-form-topic">
+                                       <label for="comment_topic">Topic <span class="required" aria-hidden="true">*</span></label>
+                                       <select required name="comment_topic" id="comment_topic">
+                                               <option value="">Select topic</option>
+                                               <option value="typo">Typo in the English text</option>
+                                       <option value="context">Where does this string appear? (more context)</option>' .
+                                               wp_kses( $language_question, array( 'option' => array( 'value' => true ) ) ) .
+                                       '</select>
+                       </p>';
+               },
+               10,
+               2
+       );
+
+       if ( is_user_logged_in() && isset( $post ) ) {
+               if ( $post instanceof Gth_Temporary_Post ) {
+                       $_post_id = $post->ID;
+                       $post_obj = $post;
+               } else {
+                       $post_obj = $post->ID;
+                       $_post_id = $post->ID;
+               }
+               comment_form(
+                       array(
+                               'title_reply'         => __( 'Discuss this string' ),
+                               /* translators: username */
+                               'title_reply_to'      => __( 'Reply to %s' ),
+                               'title_reply_before'  => '<h5 id="reply-title" class="discuss-title">',
+                               'title_reply_after'   => '</h5>',
+                               'id_form'             => 'commentform-' . $_post_id,
+                               'cancel_reply_link'   => '<span></span>',
+                               'format'              => 'html5',
+                               'comment_notes_after' => implode(
+                                       "\n",
+                                       array(
+                                               '<input type="hidden" name="comment_locale" value="' . esc_attr( $locale_slug ) . '" />',
+                                               '<input type="hidden" name="translation_id" value="' . esc_attr( $translation_id ) . '" />',
+                                               '<input type="hidden" name="redirect_to" value="' . esc_url( $original_permalink ) . '" />',
+                                       )
+                               ),
+                       ),
+                       $post_obj
+               );
+       } else {
+               /* translators: Log in URL. */
+               echo sprintf( __( 'You have to be <a href="%s">logged in</a> to comment.' ), esc_html( wp_login_url() ) );
+       }
+
+       ?>
+</div><!-- .discussion-wrapper -->
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/helpers-assets/templates/translation-discussion-comments.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_htmlwpcontentpluginsgptranslationhelpersincludesclassgpnotificationsphp"></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/gp-translation-helpers/includes/class-gp-notifications.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/gp-translation-helpers/includes/class-gp-notifications.php                               (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-gp-notifications.php 2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,476 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Routes: GP_Notifications class
+ *
+ * Manages the notifications of the plugin.
+ *
+ * @package gp-translation-helpers
+ * @since 0.0.2
+ */
+class GP_Notifications {
+       /**
+        * Sends notifications when a new comment in the discussion is stored using the WP REST API.
+        *
+        * @since 0.0.2
+        *
+        * @param WP_Comment      $comment  Inserted or updated comment object.
+        * @param WP_REST_Request $request  Request object.
+        * @param bool            $creating True when creating a comment, false when updating.
+        *
+        * @return void
+        */
+       public static function init( WP_Comment $comment, $request, $creating ) {
+               $post = get_post( $comment->comment_post_ID );
+               if ( Helper_Translation_Discussion::POST_TYPE === $post->post_type ) {
+                       if ( ( '1' === $comment->comment_approved ) || ( 'approve' === $comment->comment_approved ) ) {
+                               $comment_meta = get_comment_meta( $comment->comment_ID );
+                               if ( ( '0' !== $comment->comment_parent ) ) { // Notify to the thread only if the comment is in a thread.
+                                       self::send_emails_to_thread_commenters( $comment, $comment_meta );
+                               }
+                               $root_comment      = self::get_root_comment_in_a_thread( $comment );
+                               $root_comment_meta = get_comment_meta( $root_comment->comment_ID );
+                               if ( array_key_exists( 'comment_topic', $root_comment_meta ) ) {
+                                       switch ( $root_comment_meta['comment_topic'][0] ) {
+                                               case 'typo':
+                                               case 'context': // Notify to the GlotPress admins.
+                                                       self::send_emails_to_gp_admins( $comment, $comment_meta );
+                                                       break;
+                                               case 'question': // Notify to the project validator.
+                                                       self::send_emails_to_validators( $comment, $comment_meta );
+                                                       break;
+                                       }
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Sends notifications when a comment changes its status to "approved".
+        *
+        * @since 0.0.2
+        *
+        * @param int|string $new_status The new comment status.
+        * @param int|string $old_status The old comment status.
+        * @param WP_Comment $comment    The comment object.
+        *
+        * @return void
+        */
+       public static function on_comment_status_change( $new_status, $old_status, WP_Comment $comment ) {
+               $post = get_post( $comment->comment_post_ID );
+               if ( Helper_Translation_Discussion::POST_TYPE === $post->post_type ) {
+                       if ( $old_status != $new_status ) {
+                               if ( ( 'approved' === $new_status ) ) {
+                                       self::init( $comment, '', '' );
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Sends an email to the users that have commented on the thread, except to the last comment author.
+        *
+        * @since 0.0.2
+        *
+        * @param WP_Comment $comment      The comment object.
+        * @param array      $comment_meta The meta values for the comment.
+        *
+        * @return void
+        */
+       public static function send_emails_to_thread_commenters( WP_Comment $comment, array $comment_meta ) {
+               $parent_comments = self::get_parent_comments( $comment->comment_parent );
+               $email_addresses = self::get_commenters_email_addresses( $parent_comments, $comment->comment_author_email );
+               /**
+                * Filters the email addresses in a thread.
+                *
+                * @since 0.0.2
+                *
+                * @param array      $email_addresses       The email addresses in the thread.
+                * @param WP_Comment $comment      The comment object.
+                * @param array      $comment_meta The meta values for the comment.
+                */
+               $email_addresses = apply_filters( 'gp_notification_commenter_email_addresses', $email_addresses, $comment, $comment_meta );
+               self::send_emails( $comment, $comment_meta, $email_addresses );
+       }
+
+       /**
+        * Sends an email to the GlotPress admins.
+        *
+        * @since 0.0.2
+        *
+        * @param WP_Comment $comment      The comment object.
+        * @param array      $comment_meta The meta values for the comment.
+        *
+        * @return bool Whether the email has been sent or not.
+        */
+       public static function send_emails_to_gp_admins( WP_Comment $comment, array $comment_meta ) {
+               $email_addresses = self::get_admins_email_addresses( $comment, $comment_meta );
+               return self::send_emails( $comment, $comment_meta, $email_addresses );
+       }
+
+       /**
+        * Sends an email to the all the project validators.
+        *
+        * @since 0.0.2
+        *
+        * @param WP_Comment $comment      The comment object.
+        * @param array      $comment_meta The meta values for the comment.
+        *
+        * @return bool Whether the email has been sent or not.
+        */
+       public static function send_emails_to_validators( WP_Comment $comment, array $comment_meta ) {
+               $project = self::get_project_to_translate( $comment );
+
+               $email_addresses = self::get_validators_email_addresses( $project->path );
+               /**
+                * Filters the validators' email addresses.
+                *
+                * @since 0.0.2
+                *
+                * @param array      $email_addresses       The email addresses in the thread.
+                * @param WP_Comment $comment      The comment object.
+                * @param array      $comment_meta The meta values for the comment.
+                */
+               $email_addresses = apply_filters( 'gp_notification_validator_email_addresses', $email_addresses, $comment, $comment_meta );
+               $parent_comments = self::get_parent_comments( $comment->comment_ID ); // Includes the current comment.
+
+               $email_addresses_from_the_thread = self::get_commenters_email_addresses( $parent_comments );
+               // If one validator has a comment in the thread, we don't need to inform any other validators.
+               if ( true !== empty( array_intersect( $email_addresses, $email_addresses_from_the_thread ) ) ) {
+                       $email_addresses = array();
+               }
+
+               return self::send_emails( $comment, $comment_meta, $email_addresses );
+       }
+
+       /**
+        * Returns the comments in the thread, including the last one.
+        *
+        * @since 0.0.2
+        *
+        * @param int $comment_id Last comment of the thread.
+        *
+        * @return array The comments in the thread.
+        */
+       public static function get_parent_comments( int $comment_id ): array {
+               $comments = array();
+               $comment  = get_comment( $comment_id );
+               if ( $comment && $comment->comment_parent ) {
+                       $comments = self::get_parent_comments( $comment->comment_parent );
+               }
+               if ( ! is_null( $comment ) ) {
+                       $comments[] = $comment;
+               }
+               return $comments;
+       }
+
+       /**
+        * Gets the emails to be notified from the thread comments.
+        *
+        * Removes the second parameter from the returned array if it is found.
+        *
+        * @since 0.0.2
+        *
+        * @param array  $comments        Array with the parent comments to the posted comment.
+        * @param string $email_address_to_ignore Email from the posted comment.
+        *
+        * @return array The emails to be notified from the thread comments.
+        */
+       public static function get_commenters_email_addresses( array $comments, string $email_address_to_ignore = null ): array {
+               $email_addresses = array();
+               foreach ( $comments as $comment ) {
+                       if ( $email_address_to_ignore !== $comment->comment_author_email ) {
+                               $email_addresses[ $comment->comment_author_email ] = $comment->comment_author_email;
+                       }
+               }
+
+               return $email_addresses;
+       }
+
+       /**
+        * Gets the emails of the validators of a project.
+        *
+        * @since 0.0.2
+        *
+        * @param string $project_path The project path.
+        *
+        * @return array The emails of the validators for the given project.
+        */
+       public static function get_validators_email_addresses( string $project_path ): array {
+               $email_addresses = array();
+
+               $project = GP::$project->by_path( $project_path );
+
+               $path_to_root = array_slice( $project->path_to_root(), 1 );
+               $permissions  = GP::$validator_permission->by_project_id( $project->id );
+
+               $sort_by_locale_slug = function( $x, $y ) {
+                       return strcmp( $x->locale_slug, $y->locale_slug );
+               };
+               usort( $permissions, $sort_by_locale_slug );
+               $parent_permissions = array();
+
+               foreach ( $path_to_root as $parent_project ) {
+                       $this_parent_permissions = GP::$validator_permission->by_project_id( $parent_project->id );
+                       usort( $this_parent_permissions, $sort_by_locale_slug );
+                       foreach ( $this_parent_permissions as $permission ) {
+                               $permission->project = $parent_project;
+                       }
+                       $parent_permissions = array_merge( $parent_permissions, (array) $this_parent_permissions );
+               }
+
+               // We can't join on users table.
+               foreach ( array_merge( (array) $permissions, (array) $parent_permissions ) as $permission ) {
+                       $permission->user  = get_user_by( 'id', $permission->user_id );
+                       $email_addresses[] = $permission->user->data->user_email;
+               }
+
+               return $email_addresses;
+       }
+
+       /**
+        * Gets the email addresses of the GlotPress admins.
+        *
+        * @param WP_Comment $comment      The comment object.
+        * @param array      $comment_meta The meta values for the comment.
+        *
+        * @return array The GlotPress admins' email addresses.
+        */
+       public static function get_admins_email_addresses( WP_Comment $comment, array $comment_meta ):array {
+               global $wpdb;
+               /**
+                * Filters the validators' emails.
+                *
+                * @since 0.0.2
+                *
+                * @param array      $email_addresses       The email addresses in the thread.
+                * @param WP_Comment $comment      The comment object.
+                * @param array      $comment_meta The meta values for the comment.
+                */
+               $email_addresses = apply_filters( 'gp_notification_admin_email_addresses', array(), $comment, $comment_meta );
+               if ( ! empty( $email_addresses ) ) {
+                       return $email_addresses;
+               }
+
+               $admin_email_addresses = $wpdb->get_results(
+                       "SELECT user_email FROM {$wpdb->users}
+                       INNER JOIN {$wpdb->gp_permissions}
+                       ON {$wpdb->users}.ID = {$wpdb->gp_permissions}.user_id
+                       WHERE action='admin'"
+               );
+
+               foreach ( $admin_email_addresses as $admin ) {
+                       $email_addresses[] = $admin->user_email;
+               }
+               $parent_comments                 = self::get_parent_comments( $comment->comment_parent );
+               $email_addresses_from_the_thread = self::get_commenters_email_addresses( $parent_comments );
+               // If an admin is already involved in the thread, don't notify the other admins.
+               if ( true !== empty( array_intersect( $email_addresses, $email_addresses_from_the_thread ) ) || in_array( $comment->comment_author_email, $email_addresses, true ) ) {
+                       return array();
+               }
+
+               return array_unique( $email_addresses );
+       }
+
+       /**
+        * Sends an email to all the email addresses.
+        *
+        * @since 0.0.2
+        *
+        * @param WP_Comment $comment      The comment object.
+        * @param array      $comment_meta The meta values for the comment.
+        * @param array      $email_addresses       The email addresses that will receive the notification.
+        *
+        * @return bool Whether the email has been sent or not.
+        */
+       public static function send_emails( WP_Comment $comment, array $comment_meta, array $email_addresses ): bool {
+               /**
+                * Filters the email addresses before sending the notifications.
+                *
+                * @since 0.0.2
+                *
+                * @param array $email_addresses The email addresses to be notified.
+                */
+               $email_addresses = apply_filters( 'gp_notification_before_send_emails', $email_addresses );
+               if ( ( null === $comment ) || ( null === $comment_meta ) || ( empty( $email_addresses ) ) ) {
+                       return false;
+               }
+               $email_addresses = self::remove_commenter_email_address( $comment, $email_addresses );
+
+               $headers = array(
+                       'Content-Type: text/html; charset=UTF-8',
+               );
+               /**
+                * Filters the email headers.
+                *
+                * @since 0.0.2
+                *
+                * @param array $headers The email headers.
+                */
+               $headers = apply_filters( 'gp_notification_email_headers', $headers );
+               $subject = esc_html__( 'New comment in a translation discussion', 'glotpress' );
+               $body    = self::get_email_body( $comment, $comment_meta );
+               foreach ( $email_addresses as $bcc ) {
+                       $headers[] = 'Bcc: ' . $bcc;
+               }
+               wp_mail( '', $subject, $body, $headers );
+               return true;
+       }
+
+       /**
+        * Creates the email body message.
+        *
+        * @since 0.0.2
+        *
+        * @param WP_Comment $comment      The comment object.
+        * @param array      $comment_meta The meta values for the comment.
+        *
+        * @return string|null
+        */
+       public static function get_email_body( WP_Comment $comment, array $comment_meta ): string {
+               $project  = self::get_project_to_translate( $comment );
+               $original = self::get_original( $comment );
+               $output   = '';
+               /**
+                * Filters the content of the email at the beginning of the function that gets its content.
+                *
+                * @since 0.0.2
+                *
+                * @param string     $output       The content of the email.
+                * @param WP_Comment $comment      The comment object.
+                * @param array      $comment_meta The meta values for the comment.
+                */
+               $output  = apply_filters( 'gp_notification_pre_email_body', $output, $comment, $comment_meta );
+               $output .= esc_html__( 'Hi there,', 'glotpress' );
+               $output .= '<br><br>';
+               $url     = GP_Route_Translation_Helpers::get_permalink( $project->path, $original->id );
+               $output .= wp_kses(
+                       /* translators: The discussion URL where the user can find the comment. */
+                       sprintf( __( 'There is a new comment in a <a href="%1$s">GlotPress discussion</a> that may be of interest to you.', 'glotpress' ), $url ),
+                       array(
+                               'a' => array( 'href' => array() ),
+                       )
+               ) . '<br/>';
+               $output .= '<br>';
+               $output .= esc_html__( 'It would be nice if you have some time to review this comment and reply to it if needed.', 'glotpress' );
+               $output .= '<br><br>';
+               $output .= '- ' . wp_kses(
+                       /* translators: The discussion URL where the user can find the comment. */
+                       sprintf( __( '<strong>Discussion URL:</strong> <a href="%1$s">%1$s</a>', 'glotpress' ), $url ),
+                       array(
+                               'strong' => array(),
+                               'a'      => array( 'href' => array() ),
+                       )
+               ) . '<br/>';
+               if ( array_key_exists( 'locale', $comment_meta ) && ( ! empty( $comment_meta['locale'][0] ) ) ) {
+                       /* translators: The translation locale for the comment. */
+                       $output .= '- ' . wp_kses( sprintf( __( '<strong>Locale:</strong> %s', 'glotpress' ), $comment_meta['locale'][0] ), array( 'strong' => array() ) ) . '<br/>';
+               }
+               /* translators: The original string to translate. */
+               $output .= '- ' . wp_kses( sprintf( __( '<strong>Original string:</strong> %s', 'glotpress' ), $original->singular ), array( 'strong' => array() ) ) . '<br/>';
+               if ( array_key_exists( 'translation_id', $comment_meta ) && $comment_meta['translation_id'][0] ) {
+                       $translation_id = $comment_meta['translation_id'][0];
+                       $translation    = GP::$translation->get( $translation_id );
+                       // todo: add the plurals.
+                       if ( ! is_null( $translation ) ) {
+                               /* translators: The translation string. */
+                               $output .= '- ' . wp_kses( sprintf( __( '<strong>Translation string:</strong> %s', 'glotpress' ), $translation->translation_0 ), array( 'strong' => array() ) ) . '<br/>';
+                       }
+               }
+               /* translators: The comment made. */
+               $output .= '- ' . wp_kses( sprintf( __( '<strong>Comment:</strong> %s', 'glotpress' ), $comment->comment_content ), array( 'strong' => array() ) );
+               $output .= '<br><br>';
+               $output .= esc_html__( 'Have a nice day!', 'glotpress' );
+               $output .= '<br><br>';
+               $output .= esc_html__( 'This is an automated message. Please, do not reply directly to this email.', 'glotpress' );
+               /**
+                * Filters the content of the email at the end of the function that gets its content.
+                *
+                * @since 0.0.2
+                *
+                * @param string     $output       The content of the email.
+                * @param WP_Comment $comment      The comment object.
+                * @param array      $comment_meta The meta values for the comment.
+                */
+               $output = apply_filters( 'gp_notification_post_email_body', $output, $comment, $comment_meta );
+               return $output;
+       }
+
+       /**
+        * Gets the root comment in a thread.
+        *
+        * @since 0.0.2
+        *
+        * @param WP_Comment $comment The comment object.
+        *
+        * @return WP_Comment The root comment in the thread.
+        */
+       public static function get_root_comment_in_a_thread( WP_Comment $comment ): WP_Comment {
+               $comments = self::get_parent_comments( $comment->comment_ID );
+               foreach ( $comments as $item ) {
+                       if ( 0 === intval( $item->comment_parent ) ) {
+                               return $item;
+                       }
+               }
+               return $comment;
+       }
+
+       /**
+        * Removes the commenter email from the emails to be notified.
+        *
+        * @since 0.0.2
+        *
+        * @param WP_Comment $comment         The comment object.
+        * @param array      $email_addresses A list of emails.
+        *
+        * @return array The list of emails without the commenter's email.
+        */
+       public static function remove_commenter_email_address( WP_Comment $comment, array $email_addresses ): array {
+               $index = array_search( $comment->comment_author_email, $email_addresses, true );
+               if ( false !== $index ) {
+                       unset( $email_addresses[ $index ] );
+               }
+               return array_values( $email_addresses );
+       }
+
+       /**
+        * Gets the project that the translated string belongs to.
+        *
+        * @since 0.0.2
+        *
+        * @param WP_Comment $comment The comment object.
+        *
+        * @return GP_Project|bool The project that the translated string belongs to.
+        */
+       private static function get_project_to_translate( WP_Comment $comment ) {
+               $post_id = $comment->comment_post_ID;
+               $terms   = wp_get_object_terms( $post_id, Helper_Translation_Discussion::LINK_TAXONOMY, array( 'number' => 1 ) );
+               if ( empty( $terms ) ) {
+                       return false;
+               }
+
+               $original   = GP::$original->get( $terms[0]->slug );
+               $project_id = $original->project_id;
+               $project    = GP::$project->get( $project_id );
+
+               return $project;
+       }
+
+       /**
+        * Gets the original string that the translated string belongs to.
+        *
+        * @since 0.0.2
+        *
+        * @param WP_Comment $comment The comment object.
+        *
+        * @return GP_Thing|false The original string that the translated string belongs to.
+        */
+       private static function get_original( WP_Comment $comment ) {
+               $post_id = $comment->comment_post_ID;
+               $terms   = wp_get_object_terms( $post_id, Helper_Translation_Discussion::LINK_TAXONOMY, array( 'number' => 1 ) );
+               if ( empty( $terms ) ) {
+                       return false;
+               }
+
+               return GP::$original->get( $terms[0]->slug );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-gp-notifications.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_htmlwpcontentpluginsgptranslationhelpersincludesclassgproutetranslationhelpersphp"></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/gp-translation-helpers/includes/class-gp-route-translation-helpers.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/gp-translation-helpers/includes/class-gp-route-translation-helpers.php                           (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-gp-route-translation-helpers.php     2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,337 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Routes: GP_Route_Translation_Helpers class
+ *
+ * @package gp-translation-helpers
+ * @since 0.0.1
+ */
+class GP_Route_Translation_Helpers extends GP_Route {
+
+       /**
+        * Stores an instance of each helper.
+        *
+        * @since 0.0.1
+        * @var array
+        */
+       private $helpers = array();
+
+       /**
+        * GP_Route_Translation_Helpers constructor.
+        *
+        * @since 0.0.1
+        */
+       public function __construct() {
+               $this->helpers       = GP_Translation_Helpers::load_helpers();
+               $this->template_path = dirname( __FILE__ ) . '/../templates/';
+       }
+
+       /**
+        * Loads the 'original-permalink' template.
+        *
+        * @since 0.0.2
+        *
+        * @param string      $project_path         The project path. E.g. "wp/dev".
+        * @param int         $original_id          The original id. E.g. "2440".
+        * @param string|null $locale_slug          Optional. The locale slug. E.g. "es".
+        * @param string      $translation_set_slug The translation slug. E.g. "default".
+        * @param int|null    $translation_id       Optional. The translation id. E.g. "4525".
+        *
+        * @return void
+        */
+       public function original_permalink( $project_path, $original_id, $locale_slug = null, $translation_set_slug = null, $translation_id = null ) {
+               $original = GP::$original->get( $original_id );
+               if ( ! $original ) {
+                       $this->die_with_404();
+               }
+               $project = GP::$project->by_path( $project_path );
+               if ( ! $project ) {
+                       $this->die_with_404();
+               }
+
+               if ( $project->id !== $original->project_id ) {
+                       $project = GP::$project->get( $original->project_id );
+
+                       // Let's use the parameters that we have to create a URL in the right project.
+                       $corrected_url = self::get_permalink( $project->path, $original_id, $locale_slug, $translation_set_slug );
+
+                       wp_safe_redirect( $corrected_url );
+                       exit;
+               }
+
+               if ( ! $original ) {
+                       $this->die_with_404();
+               }
+
+               $args = array(
+                       'project_id'     => $project->id,
+                       'locale_slug'    => $locale_slug,
+                       'set_slug'       => $translation_set_slug,
+                       'original_id'    => $original_id,
+                       'translation_id' => $translation_id,
+                       'project'        => $project,
+
+               );
+               $translation_set = GP::$translation_set->by_project_id_slug_and_locale( $project->id, $translation_set_slug, $locale_slug );
+
+               $all_translation_sets = GP::$translation_set->by_project_id( $project->id );
+
+               $row_id      = $original_id;
+               $translation = null;
+               if ( $translation_id ) {
+                       $row_id     .= '-' . $translation_id;
+                       $translation = GP::$translation->get( $translation_id );
+               }
+               $original_permalink = gp_url_project( $project, array( 'filters[original_id]' => $original_id ) );
+
+               $original_translation_permalink = false;
+               if ( $translation_set ) {
+                       $original_translation_permalink = gp_url_project_locale( $project, $locale_slug, $translation_set->slug, array( 'filters[original_id]' => $original_id ) );
+               }
+
+               /** Get translation for this original */
+               $existing_translations = array();
+               if ( ! $translation && $translation_set && $original_id ) {
+                       $translation = GP::$translation->find_one(
+                               array(
+                                       'status'             => 'current',
+                                       'original_id'        => $original_id,
+                                       'translation_set_id' => $translation_set->id,
+                               )
+                       );
+
+                       if ( ! $translation ) {
+                               $existing_translations = GP::$translation->find_many_no_map(
+                                       array(
+                                               'original_id'        => $original_id,
+                                               'translation_set_id' => $translation_set->id,
+                                       )
+                               );
+                               usort(
+                                       $existing_translations,
+                                       function ( $t1, $t2 ) {
+                                               $cmp_prop_t1 = $t1->date_modified ?? $t1->date_added;
+                                               $cmp_prop_t2 = $t2->date_modified ?? $t2->date_added;
+                                               return $cmp_prop_t1 < $cmp_prop_t2;
+                                       }
+                               );
+
+                               // Something falsy is not enough.
+                               $translation = null;
+                       }
+               }
+
+               $priorities_key_value = $original->get_static( 'priorities' );
+               $priority             = $priorities_key_value[ $original->priority ];
+
+               $args     = compact( 'project', 'locale_slug', 'translation_set_slug', 'original_id', 'translation_id', 'translation', 'original_permalink' );
+               $sections = $this->get_translation_helper_sections( $args );
+
+               $translations       = GP::$translation->find_many_no_map(
+                       array(
+                               'status'      => 'current',
+                               'original_id' => $original_id,
+                       )
+               );
+               $no_of_translations = count( $translations );
+
+               add_action(
+                       'gp_head',
+                       function() use ( $original, $no_of_translations ) {
+                               echo '<meta property="og:title" content="' . esc_html( $original->singular ) . ' | ' . esc_html( $no_of_translations ) . ' translations" />';
+                       }
+               );
+
+               $this->tmpl( 'original-permalink', get_defined_vars() );
+       }
+
+       /**
+        * Gets the sections of each active helper.
+        *
+        * @param      array $data   The data to be passed on to the sections.
+        *
+        * @return     array   The translation helper sections.
+        */
+       public function get_translation_helper_sections( $data ) {
+               $sections = array();
+               foreach ( $this->helpers as $helper => $translation_helper ) {
+                       $translation_helper->set_data( $data );
+
+                       if ( ! $translation_helper->activate() ) {
+                               continue;
+                       }
+
+                       $sections[] = array(
+                               'title'             => $translation_helper->get_title(),
+                               'content'           => $translation_helper->get_output(),
+                               'classname'         => $translation_helper->get_div_classname(),
+                               'id'                => $translation_helper->get_div_id(),
+                               'priority'          => $translation_helper->get_priority(),
+                               'has_async_content' => $translation_helper->has_async_content(),
+                               'count'             => $translation_helper->get_count(),
+                               'load_inline'       => $translation_helper->load_inline(),
+                               'helper'            => $helper,
+                       );
+               }
+
+               usort(
+                       $sections,
+                       function( $s1, $s2 ) {
+                               return $s1['priority'] > $s2['priority'];
+                       }
+               );
+
+               return $sections;
+       }
+
+       /**
+        * Returns the content of each section (tab).
+        *
+        * @since 0.0.2
+        *
+        * @param string   $project_path    The project path. E.g. "wp/dev".
+        * @param string   $locale_slug     The locale slug. E.g. "es".
+        * @param string   $set_slug        The translation set slug. E.g. "default".
+        * @param int      $original_id     The original id. E.g. "2440".
+        * @param int|null $translation_id  Optional. The translation id. E.g. "4525".
+        *
+        * @return string                   JSON with the content of each section.
+        */
+       public function ajax_translation_helpers_locale( string $project_path, string $locale_slug, string $set_slug, int $original_id, int $translation_id = null ) {
+               return $this->ajax_translation_helpers( $project_path, $original_id, $translation_id, $locale_slug, $set_slug );
+       }
+
+       /**
+        * Returns the content of each section (tab).
+        *
+        * @since 0.0.1
+        *
+        * @param string      $project_path     The project path. E.g. "wp/dev".
+        * @param int         $original_id      The original id. E.g. "2440".
+        * @param int|null    $translation_id   Optional. The translation id. E.g. "4525".
+        * @param string|null $locale_slug      The locale slug. E.g. "es".
+        * @param string|null $set_slug         The translation set slug. E.g. "default".
+        *
+        * @return void                         Prints the JSON with the content of each section.
+        */
+       public function ajax_translation_helpers( string $project_path, int $original_id, int $translation_id = null, string $locale_slug = null, string $set_slug = null ): void {
+               $project = GP::$project->by_path( $project_path );
+               if ( ! $project ) {
+                       $this->die_with_404();
+               }
+
+               $permalink = self::get_permalink( $project->path, $original_id, $set_slug, $locale_slug );
+
+               $args = array(
+                       'project_id'           => $project->id,
+                       'locale_slug'          => $locale_slug,
+                       'translation_set_slug' => $set_slug,
+                       'original_id'          => $original_id,
+                       'translation_id'       => $translation_id,
+                       'permalink'            => $permalink,
+                       'project'              => $project,
+               );
+
+               $selected = gp_get( 'helpers' );
+               if ( ! empty( $selected ) ) {
+                       $helpers = array_filter(
+                               $this->helpers,
+                               function ( $key ) use ( $selected ) {
+                                       return in_array( $key, (array) $selected, true );
+                               },
+                               ARRAY_FILTER_USE_KEY
+                       );
+               } else {
+                       $helpers = $this->helpers;
+               }
+
+               $sections = array();
+               foreach ( $helpers as $translation_helper ) {
+                       $translation_helper->set_data( $args );
+                       if ( $translation_helper->has_async_content() && $translation_helper->activate() ) {
+                               $sections[ $translation_helper->get_div_id() ] = array(
+                                       'content' => $translation_helper->get_async_output(),
+                                       'count'   => $translation_helper->get_count(),
+                               );
+                       };
+               }
+
+               echo wp_json_encode( $sections );
+       }
+
+       /**
+        * Gets the locales with comments.
+        *
+        * @since 0.0.2
+        *
+        * @param array|null $comments  Array with comments.
+        *
+        * @return array                Array with the locales with comments.
+        */
+       private function get_locales_with_comments( ?array $comments ): array {
+               $comment_locales = array();
+               if ( $comments ) {
+                       foreach ( $comments as $comment ) {
+                               $comment_meta          = get_comment_meta( $comment->comment_ID, 'locale' );
+                               $single_comment_locale = is_array( $comment_meta ) && ! empty( $comment_meta ) ? $comment_meta[0] : '';
+                               if ( $single_comment_locale && ! in_array( $single_comment_locale, $comment_locales, true ) ) {
+                                       $comment_locales[] = $single_comment_locale;
+                               }
+                       }
+               }
+               return $comment_locales;
+       }
+
+       /**
+        * Gets the full permalink.
+        *
+        * @since 0.0.2
+        *
+        * @param string      $project_path The project path. E.g. "wp/dev".
+        * @param int|null    $original_id  The original id. E.g. "2440".
+        * @param string|null $set_slug     The translation set slug. E.g. "default".
+        * @param string|null $locale_slug  Optional. The locale slug. E.g. "es".
+        *
+        * @return string                   The full permalink.
+        */
+       public static function get_permalink( string $project_path, ?int $original_id, string $set_slug = null, string $locale_slug = null ): string {
+               $permalink = $project_path . '/' . $original_id;
+               if ( $set_slug && $locale_slug ) {
+                       $permalink .= '/' . $locale_slug . '/' . $set_slug;
+               }
+               return home_url( gp_url_project( $permalink ) );
+       }
+
+       /**
+        * Gets the translation permalink.
+        *
+        * @param      GP_Project $project               The project.
+        * @param      string     $locale_slug           The locale slug.
+        * @param      string     $translation_set_slug  The translation set slug.
+        * @param      int        $original_id           The original id.
+        * @param      int        $translation_id        The translation id.
+        *
+        * @return     bool    The translation permalink.
+        */
+       public static function get_translation_permalink( $project, $locale_slug, $translation_set_slug, $original_id, $translation_id = null ) {
+               if ( ! $project || ! $locale_slug || ! $translation_set_slug || ! $original_id ) {
+                       return false;
+               }
+
+               $args = array(
+                       'filters[original_id]' => $original_id,
+               );
+
+               if ( $translation_id ) {
+                       $args['filters[status]']         = 'either';
+                       $args['filters[translation_id]'] = $translation_id;
+               }
+
+               $translation_permalink = gp_url_project_locale(
+                       $project,
+                       $locale_slug,
+                       $translation_set_slug,
+                       $args
+               );
+               return $translation_permalink;
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-gp-route-translation-helpers.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_htmlwpcontentpluginsgptranslationhelpersincludesclassgptranslationhelpersphp"></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/gp-translation-helpers/includes/class-gp-translation-helpers.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/gp-translation-helpers/includes/class-gp-translation-helpers.php                         (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-gp-translation-helpers.php   2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,446 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * GP_Translation_Helpers class
+ *
+ * @package gp-translation-helpers
+ * @since 0.0.1
+ */
+class GP_Translation_Helpers {
+
+       /**
+        * Class id.
+        *
+        * @since 0.0.1
+        * @var string
+        */
+       public $id = 'translation-helpers';
+
+       /**
+        * Stores an instance of each helper.
+        *
+        * @since 0.0.1
+        * @var array
+        */
+       private $helpers = array();
+
+       /**
+        * Stores the self instance.
+        *
+        * @since 0.0.1
+        * @var object
+        */
+       private static $instance = null;
+
+       /**
+        * Inits the class.
+        *
+        * @since 0.0.1
+        *
+        * @return void
+        */
+       public static function init() {
+               self::get_instance();
+       }
+
+       public static function get_instance() {
+               if ( is_null( self::$instance ) ) {
+                       self::$instance = new self();
+               }
+
+               return self::$instance;
+       }
+
+       /**
+        * GP_Translation_Helpers constructor.
+        *
+        * @since 0.0.1
+        */
+       public function __construct() {
+               add_action( 'template_redirect', array( $this, 'register_routes' ), 5 );
+               add_action( 'gp_before_request', array( $this, 'before_request' ), 10, 2 );
+               add_action( 'rest_after_insert_comment', array( 'GP_Notifications', 'init' ), 10, 3 );
+               add_action( 'transition_comment_status', array( 'GP_Notifications', 'on_comment_status_change' ), 10, 3 );
+               add_action( 'gp_pre_tmpl_load', array( $this, 'register_reject_feedback_js' ), 10, 2 );
+               add_action( 'wp_ajax_reject_with_feedback', array( $this, 'reject_with_feedback' ) );
+
+               add_thickbox();
+               gp_enqueue_style( 'thickbox' );
+
+               wp_register_style( 'gp-discussion-css', plugins_url( '/../css/discussion.css', __FILE__ ), array(), '0.0.1' );
+               gp_enqueue_style( 'gp-discussion-css' );
+
+               add_filter( 'gp_translation_row_template_more_links', array( $this, 'translation_row_template_more_links' ), 10, 5 );
+               add_filter( 'preprocess_comment', array( $this, 'preprocess_comment' ) );
+
+               $this->helpers = self::load_helpers();
+       }
+
+       /**
+        * Adds the action to load the CSS and JavaScript required only in the original-permalink template.
+        *
+        * @since 0.0.1
+        *
+        * @param string $class_name    The class name of the route.
+        * @param string $last_method   The route method that will be called.
+        *
+        * @return void
+        */
+       public function before_request( string $class_name, string $last_method ) {
+               if (
+                       in_array(
+                               $class_name . '::' . $last_method,
+                               array(
+                                       // 'GP_Route_Translation::translations_get',
+                                       'GP_Route_Translation_Helpers::original_permalink',
+                               ),
+                               true
+                       )
+               ) {
+                       add_action( 'gp_pre_tmpl_load', array( $this, 'pre_tmpl_load' ), 10, 2 );
+               }
+       }
+
+       /**
+        * Adds the link for the discussion in the main screen.
+        *
+        * @since 0.0.2
+        *
+        * @todo Move the inline CSS style to a CSS file when this plugin be integrated into GlotPress.
+        *
+        * @param array              $more_links         The links to be output.
+        * @param GP_Project         $project            Project object.
+        * @param GP_Locale          $locale             Locale object.
+        * @param GP_Translation_Set $translation_set    Translation Set object.
+        * @param Translation_Entry  $translation        Translation object.
+        *
+        * @return array
+        */
+       public function translation_row_template_more_links( array $more_links, GP_Project $project, GP_Locale $locale, GP_Translation_Set $translation_set, Translation_Entry $translation ): array {
+               $permalink = GP_Route_Translation_Helpers::get_permalink( $project->path, $translation->original_id, $translation_set->slug, $translation_set->locale );
+
+               $links                    = '<a href="' . esc_url( $permalink ) . '">Discussion</a>';
+               $links                   .= '<a href="' . esc_url( $permalink ) . '" style="float:right" target="_blank"><span class="dashicons dashicons-external"></span></a>';
+               $more_links['discussion'] = $links;
+
+               return $more_links;
+       }
+
+       /**
+        * Prevents remote POST to comment forms.
+        *
+        * @since 0.0.2
+        *
+        * @param array $commentdata     Comment data.
+        *
+        * @return array|void
+        */
+       public function preprocess_comment( array $commentdata ) {
+               if ( ! $commentdata['user_ID'] ) {
+                       die( 'User not authorized!' );
+               }
+                               return $commentdata;
+       }
+
+       /**
+        * Loads the CSS and JavaScript required only in the original-permalink template.
+        *
+        * @since 0.0.1
+        *
+        * @param string $template  The template. E.g. "original-permalink".
+        * @param array  $args      Arguments passed to the template. Passed by reference.
+        *
+        * @return void
+        */
+       public function pre_tmpl_load( string $template, array $args ):void {
+               $allowed_templates = apply_filters( 'gp_translations_helpers_templates', array( 'original-permalink' ) );
+
+               if ( ! in_array( $template, $allowed_templates, true ) ) {
+                       return;
+               }
+
+               $translation_helpers_settings = array(
+                       'th_url' => gp_url_project( $args['project'], gp_url_join( $args['locale_slug'], $args['translation_set_slug'], '-get-translation-helpers' ) ),
+               );
+
+               add_action( 'gp_head', array( $this, 'css_and_js' ), 10 );
+               add_action( 'gp_translation_row_editor_columns', array( $this, 'translation_helpers' ), 10, 2 );
+
+               add_filter(
+                       'gp_translation_row_editor_clospan',
+                       function( $colspan ) {
+                               return ( $colspan - 3 );
+                       }
+               );
+
+               wp_register_style( 'gp-translation-helpers-css', plugins_url( 'css/translation-helpers.css', __DIR__ ), '', '0.0.1' ); // todo: add the version as global element.
+               gp_enqueue_style( 'gp-translation-helpers-css' );
+
+               wp_register_script( 'gp-translation-helpers', plugins_url( '/js/translation-helpers.js', __DIR__ ), array( 'gp-editor' ), '2017-02-09', true );
+               gp_enqueue_scripts( array( 'gp-translation-helpers' ) );
+
+               wp_localize_script( 'gp-translation-helpers', '$gp_translation_helpers_settings', $translation_helpers_settings );
+               wp_localize_script(
+                       'gp-translation-helpers',
+                       'wpApiSettings',
+                       array(
+                               'root'           => esc_url_raw( rest_url() ),
+                               'nonce'          => wp_create_nonce( 'wp_rest' ),
+                               'admin_ajax_url' => admin_url( 'admin-ajax.php' ),
+                       )
+               );
+       }
+
+       /**
+        * Gets the translation helpers.
+        *
+        * The returned array has the title helper as key and object of
+        * this class as value.
+        *
+        * @since 0.0.1
+        *
+        * @return array
+        */
+       public static function load_helpers(): array {
+               $base_dir = dirname( dirname( __FILE__ ) ) . '/helpers/';
+               require_once $base_dir . '/base-helper.php';
+
+               $helpers_files = array(
+                       'helper-translation-discussion.php',
+                       'helper-other-locales.php',
+                       'helper-translation-history.php',
+               );
+
+               foreach ( $helpers_files as $helper ) {
+                       require_once $base_dir . $helper;
+               }
+
+               $helpers = array();
+
+               $classes = get_declared_classes();
+               foreach ( $classes as $declared_class ) {
+                       $reflect = new ReflectionClass( $declared_class );
+                       if ( $reflect->isSubclassOf( 'GP_Translation_Helper' ) ) {
+                               $helpers[ sanitize_title_with_dashes( $reflect->getDefaultProperties()['title'] ) ] = new $declared_class();
+                       }
+               }
+
+               return $helpers;
+       }
+
+       /**
+        * Loads the 'translation-helpers' template.
+        *
+        * @since 0.0.1
+        *
+        * @param GP_Translation     $translation The current translation.
+        * @param GP_Translation_Set $translation_set The current translation set.
+        *
+        * @return void
+        */
+       public function translation_helpers( GP_Translation $translation, GP_Translation_Set $translation_set ) {
+               $args = array(
+                       'project_id'           => $translation->project_id,
+                       'locale_slug'          => $translation_set->locale,
+                       'translation_set_slug' => $translation_set->slug,
+                       'original_id'          => $translation->original_id,
+                       'translation_id'       => $translation->id,
+                       'translation'          => $translation,
+               );
+
+               $sections = array();
+               foreach ( $this->helpers as $translation_helper ) {
+                       $translation_helper->set_data( $args );
+
+                       if ( ! $translation_helper->activate() ) {
+                               continue;
+                       }
+
+                       $sections[] = array(
+                               'title'             => $translation_helper->get_title(),
+                               'content'           => $translation_helper->get_output(),
+                               'classname'         => $translation_helper->get_div_classname(),
+                               'id'                => $translation_helper->get_div_id(),
+                               'priority'          => $translation_helper->get_priority(),
+                               'has_async_content' => $translation_helper->has_async_content(),
+                               'load_inline'       => $translation_helper->load_inline(),
+                       );
+               }
+               usort(
+                       $sections,
+                       function( $s1, $s2 ) {
+                               return $s1['priority'] > $s2['priority'];
+                       }
+               );
+
+               gp_tmpl_load( 'translation-helpers', array( 'sections' => $sections ), dirname( __FILE__ ) . '/templates/' );
+       }
+
+       /**
+        * Registers the routes and the methods that will respond to these routes.
+        *
+        * @since 0.0.1
+        *
+        * @return void
+        */
+       public function register_routes() {
+               $dir      = '([^_/][^/]*)';
+               $path     = '(.+?)';
+               $projects = 'projects';
+               $project  = $projects . '/' . $path;
+               $locale   = '(' . implode( '|', wp_list_pluck( GP_Locales::locales(), 'slug' ) ) . ')';
+               $set      = "$project/$locale/$dir";
+               $id       = '(\d+)-?(\d+)?';
+
+               GP::$router->prepend( "/$project/(\d+)(?:/$locale/$dir)?(/\d+)?", array( 'GP_Route_Translation_Helpers', 'original_permalink' ), 'get' );
+               GP::$router->prepend( "/$project/-get-translation-helpers/$id", array( 'GP_Route_Translation_Helpers', 'ajax_translation_helpers' ), 'get' );
+               GP::$router->prepend( "/$project/$locale/$dir/-get-translation-helpers/$id", array( 'GP_Route_Translation_Helpers', 'ajax_translation_helpers_locale' ), 'get' );
+       }
+
+       /**
+        * Prints inline CSS and JavaScript.
+        *
+        * @since 0.0.1
+        *
+        * @return void
+        */
+       public function css_and_js() {
+               ?>
+               <style>
+                       <?php
+                       foreach ( $this->helpers as $translation_helper ) {
+                               $css = $translation_helper->get_css();
+                               if ( $css ) {
+                                       echo '/* Translation Helper:  ' . esc_js( $translation_helper->get_title() ) . ' */' . "\n";
+                                       echo esc_html( $css ) . "\n";
+                               }
+                       }
+                       ?>
+               </style>
+               <script>
+                       <?php
+                       foreach ( $this->helpers as $translation_helper ) {
+                               $js = $translation_helper->get_js();
+                               if ( $js ) {
+                                       echo '/* Translation Helper:  ' . esc_js( $translation_helper->get_title() ) . ' */' . "\n";
+                                       echo $js . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+                               }
+                       }
+                       ?>
+               </script>
+               <?php
+       }
+
+       /**
+        * Registers and enqueues the reject-feedback.js script and also loads a few js variables on the page.
+        *
+        * @since 0.0.2
+        *
+        *  @param string $template Template of the current page.
+        *  @param string $translation_set Current translation set
+        *
+        * @return void
+        */
+       public function register_reject_feedback_js( $template, $translation_set ) {
+
+               if ( 'translations' !== $template ) {
+                       return;
+               }
+
+               wp_register_script( 'gp-reject-feedback-js', plugins_url( '/../js/reject-feedback.js', __FILE__ ), array( 'jquery', 'gp-common', 'gp-editor', 'thickbox' ), '0.0.1' );
+               gp_enqueue_script( 'gp-reject-feedback-js' );
+
+               wp_localize_script(
+                       'gp-reject-feedback-js',
+                       '$gp_reject_feedback_settings',
+                       array(
+                               'url'            => admin_url( 'admin-ajax.php' ),
+                               'nonce'          => wp_create_nonce( 'gp_reject_feedback' ),
+                               'locale_slug'    => $translation_set['locale_slug'],
+                               'reject_reasons' => Helper_Translation_Discussion::get_reject_reasons(),
+                       )
+               );
+       }
+
+       /**
+        * Is called from the AJAX request in reject-feedback.js to submit a rejection feedback.
+        *
+        * @since 0.0.2
+        *
+        * @return void
+        */
+       public function reject_with_feedback() {
+               check_ajax_referer( 'gp_reject_feedback', 'nonce' );
+
+               $helper_discussion    = new Helper_Translation_Discussion();
+               $locale_slug          = $helper_discussion->sanitize_comment_locale( sanitize_text_field( $_POST['data']['locale_slug'] ) );
+               $translation_id_array = ! empty( $_POST['data']['translation_id'] ) ? array_map( array( $helper_discussion, 'sanitize_translation_id' ), $_POST['data']['translation_id'] ) : null;
+               $original_id_array    = ! empty( $_POST['data']['original_id'] ) ? array_map( array( $helper_discussion, 'sanitize_original_id' ), $_POST['data']['original_id'] ) : null;
+               $reject_reason        = ! empty( $_POST['data']['reason'] ) ? $_POST['data']['reason'] : array( 'other' );
+               $all_reject_reasons   = array_keys( Helper_Translation_Discussion::get_reject_reasons() );
+               $reject_reason        = array_filter(
+                       $reject_reason,
+                       function( $reason ) use ( $all_reject_reasons ) {
+                               return in_array( $reason, $all_reject_reasons );
+                       }
+               );
+               $reject_comment       = sanitize_text_field( $_POST['data']['comment'] );
+
+               if ( ! $locale_slug || ! $translation_id_array || ! $original_id_array || ( ! $reject_reason && ! $reject_comment ) ) {
+                       wp_send_json_error();
+               }
+
+               // Get original_id and translation_id of first string in the array
+               $first_original_id    = array_shift( $original_id_array );
+               $first_translation_id = array_shift( $translation_id_array );
+
+               // Post comment on discussion page for the first string
+               $save_feedback = $this->insert_reject_comment( $reject_comment, $first_original_id, $reject_reason, $first_translation_id, $locale_slug, $_SERVER );
+
+               if ( ! empty( $original_id_array ) && ! empty( $translation_id_array ) ) {
+                       // For other strings post link to the comment.
+                       $reject_comment = get_comment_link( $save_feedback );
+                       foreach ( $original_id_array as $index => $single_original_id ) {
+                               $this->insert_reject_comment( $reject_comment, $single_original_id, $reject_reason, $translation_id_array[ $index ], $locale_slug, $_SERVER );
+                       }
+               }
+
+               wp_send_json_success();
+       }
+
+       /**
+        * Inserts rejection feedback as WordPress comment
+        *
+        * @since 0.0.2
+        *
+        *  @param string $reject_comment Feedback entered by reviewer.
+        *  @param int    $original_id ID of the original where the comment will be added.
+        *  @param array  $reject_reason Reason(s) for rejection.
+        *  @param string $translation_id ID of the rejected translation.
+        *  @param string $locale_slug    Locale of the rejected translation.
+        *  @param array  $server         The $_SERVER array
+        *
+        * @return false|int
+        */
+       private function insert_reject_comment( $reject_comment, $original_id, $reject_reason, $translation_id, $locale_slug, $server ) {
+               $post_id = Helper_Translation_Discussion::get_or_create_shadow_post_id( $original_id );
+               $user    = wp_get_current_user();
+               return wp_insert_comment(
+                       array(
+                               'comment_post_ID'      => $post_id,
+                               'comment_author'       => $user->display_name,
+                               'comment_author_email' => $user->user_email,
+                               'comment_author_url'   => $user->user_url,
+                               'comment_author_IP'    => sanitize_text_field( $server['REMOTE_ADDR'] ),
+                               'comment_content'      => $reject_comment,
+                               'comment_agent'        => sanitize_text_field( $server['HTTP_USER_AGENT'] ),
+                               'user_id'              => $user->ID,
+                               'comment_meta'         => array(
+                                       'reject_reason'  => $reject_reason,
+                                       'translation_id' => $translation_id,
+                                       'locale'         => $locale_slug,
+                               ),
+                       )
+               );
+       }
+
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-gp-translation-helpers.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_htmlwpcontentpluginsgptranslationhelpersincludesclassgthtemporarypostphp"></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/gp-translation-helpers/includes/class-gth-temporary-post.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/gp-translation-helpers/includes/class-gth-temporary-post.php                             (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-gth-temporary-post.php       2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,14 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+class Gth_Temporary_Post {
+       public $ID;
+       public $comments_open = 'open';
+       public function __construct( $post_id ) {
+               $this->ID = $post_id;
+       }
+
+       public function __toString() {
+               return strval( $this->ID );
+       }
+}
+
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-gth-temporary-post.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_htmlwpcontentpluginsgptranslationhelpersincludesclasswporgnotificationsphp"></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/gp-translation-helpers/includes/class-wporg-notifications.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/gp-translation-helpers/includes/class-wporg-notifications.php                            (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-wporg-notifications.php      2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,408 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Routes: WPorg_Notifications class
+ *
+ * Manages the WPorg notifications in the translation notifications.
+ *
+ * @package gp-translation-helpers
+ * @since 0.0.2
+ */
+class WPorg_GlotPress_Notifications {
+       /**
+        * Emails to receive the comments about typos and asking for feedback in core, patterns, meta and apps.
+        *
+        * @todo Update these emails to the correct ones.
+        *
+        * @since 0.0.2
+        * @var array
+        */
+       private static $i18n_email = array(
+               'i18n@wordpress.org',
+               'i18n2@wordpress.org',
+       );
+
+       /**
+        * Adds the hooks to modify the email authors, validators and the email body.
+        *
+        * @since 0.0.2
+        *
+        * @return void
+        */
+       public static function init() {
+               if ( defined( 'WPORG_TRANSLATE_BLOGID' ) && ( get_current_blog_id() === WPORG_TRANSLATE_BLOGID ) ) {
+                       add_filter(
+                               'gp_notification_admin_email_addresses',
+                               function ( $email_addresses, $comment, $comment_meta ) {
+                                       return self::get_author_email_address( $comment, $comment_meta );
+                               },
+                               10,
+                               3
+                       );
+                       add_filter(
+                               'gp_notification_validator_email_addresses',
+                               function ( $email_addresses, $comment, $comment_meta ) {
+                                       return self::get_validator_email_addresses( $comment, $comment_meta );
+                               },
+                               10,
+                               3
+                       );
+                       add_filter(
+                               'gp_notification_post_email_body',
+                               function ( $output, $comment, $comment_meta ) {
+                                       return self::get_email_body( $comment, $comment_meta );
+                               },
+                               10,
+                               3
+                       );
+                       add_filter(
+                               'gp_notification_before_send_emails',
+                               function ( $email_addresses ) {
+                                       return self::get_opted_in_email_addresses( $email_addresses );
+                               },
+                               10,
+                               1
+                       );
+                       add_filter(
+                               'gp_notification_email_headers',
+                               function () {
+                                       return array(
+                                               'Content-Type: text/html; charset=UTF-8',
+                                               'From: Translating WordPress.org <no-reply@wordpress.org>',
+                                       );
+                               }
+                       );
+               }
+       }
+
+       /**
+        * Gets the email addresses of all project validators: GTE, PTE and CLPTE.
+        *
+        * Returns an empty array if one GTE/PTE/CLPTE has a comment in the thread,
+        * so only one validators is notified.
+        *
+        * @since 0.0.2
+        *
+        * @param WP_Comment $comment      The comment object.
+        * @param array      $comment_meta The meta values for the comment.
+        *
+        * @return array    The validators' emails.
+        */
+       public static function get_validator_email_addresses( WP_Comment $comment, array $comment_meta ): array {
+               $locale                 = $comment_meta['locale'][0];
+               $email_addresses        = self::get_gte_email_addresses( $locale );
+               $email_addresses        = array_merge( $email_addresses, self::get_pte_email_addresses_by_project_and_locale( $comment, $locale ) );
+               $email_addresses        = array_merge( $email_addresses, self::get_clpte_email_addresses_by_project( $comment ) );
+               $parent_comments        = GP_Notifications::get_parent_comments( $comment->comment_parent );
+               $emails_from_the_thread = GP_Notifications::get_commenters_email_addresses( $parent_comments );
+               // Set the email addresses array as empty if one GTE/PTE/CLPTE has a comment in the thread.
+               if ( ! empty( array_intersect( $email_addresses, $emails_from_the_thread ) ) || in_array( $comment->comment_author_email, $email_addresses, true ) ) {
+                       return array();
+               }
+               return $email_addresses;
+       }
+
+       /**
+        * Gets the general translation editors (GTE) emails for the given locale.
+        *
+        * @since 0.0.2
+        *
+        * @param string $locale The locale. E.g. 'zh-tw'.
+        *
+        * @return array The general translation editors (GTE) emails.
+        */
+       public static function get_gte_email_addresses( string $locale ): array {
+               $email_addresses = array();
+
+               $gp_locale = GP_Locales::by_field( 'slug', $locale );
+               if ( ( ! defined( 'WPORG_TRANSLATE_BLOGID' ) ) || ( false === $gp_locale ) ) {
+                       return $email_addresses;
+               }
+               $result  = get_sites(
+                       array(
+                               'locale'     => $gp_locale->wp_locale,
+                               'network_id' => WPORG_GLOBAL_NETWORK_ID,
+                               'path'       => '/',
+                               'fields'     => 'ids',
+                               'number'     => '1',
+                       )
+               );
+               $site_id = array_shift( $result );
+               if ( ! $site_id ) {
+                       return $email_addresses;
+               }
+
+               $users = get_users(
+                       array(
+                               'blog_id'     => $site_id,
+                               'role'        => 'general_translation_editor',
+                               'count_total' => false,
+                       )
+               );
+               foreach ( $users as $user ) {
+                       $email_addresses[] = $user->data->user_email;
+               }
+
+               return $email_addresses;
+       }
+
+       /**
+        * Gets the project translation editors (PTE) emails for the given translation_id (from a project) and locale.
+        *
+        * @since 0.0.2
+        *
+        * @param WP_Comment $comment The comment object.
+        * @param string     $locale  The locale. E.g. 'zh-tw'.
+        *
+        * @return array The project translation editors (PTE) emails.
+        */
+       public static function get_pte_email_addresses_by_project_and_locale( $comment, $locale ): array {
+               return self::get_pte_clpte_email_addresses_by_project_and_locale( $comment, $locale );
+       }
+
+       /**
+        * Gets the cross language project translation editors (CLPTE) emails for the given translation_id (from a project).
+        *
+        * @since 0.0.2
+        *
+        * @param WP_Comment $comment The comment object.
+        *
+        * @return array The cross language project translation editors (CLPTE) emails.
+        */
+       public static function get_clpte_email_addresses_by_project( $comment ): array {
+               return self::get_pte_clpte_email_addresses_by_project_and_locale( $comment, 'all-locales' );
+       }
+
+       /**
+        * Gets the PTE/CLPTE emails for the given translation_id (from a project) and locale.
+        *
+        * @since 0.0.2
+        *
+        * @param WP_Comment $comment The comment object.
+        * @param string     $locale  The locale. E.g. 'zh-tw'.
+        *
+        * @return array The PTE/CLPTE emails for the project and locale.
+        */
+       private static function get_pte_clpte_email_addresses_by_project_and_locale( WP_Comment $comment, string $locale ): array {
+               global $wpdb;
+
+               if ( 'all-locales' === $locale ) {
+                       $gp_locale = 'all-locales';
+               } else {
+                       $gp_locale = GP_Locales::by_field( 'slug', $locale );
+               }
+
+               if ( ( ! defined( 'WPORG_TRANSLATE_BLOGID' ) ) || ( false === $gp_locale ) ) {
+                       return $array();
+               }
+
+               $project = self::get_project_to_translate( $comment );
+
+               // todo: remove the deleted users in the SQL query.
+               $translation_editors = $wpdb->get_results(
+                       $wpdb->prepare(
+                               "
+                       SELECT
+                               {$wpdb->wporg_translation_editors}.user_id, 
+                           {$wpdb->wporg_translation_editors}.locale
+                       FROM {$wpdb->wporg_translation_editors}
+                       WHERE {$wpdb->wporg_translation_editors}.project_id = %d AND
+                             {$wpdb->wporg_translation_editors}.locale = %s 
+               ",
+                               $project->id,
+                               $locale
+                       )
+               );
+
+               $email_addresses = array();
+               foreach ( $translation_editors as $pte ) {
+                       $email_addresses[] = get_user_by( 'id', $pte->user_id )->user_email;
+               }
+               return $email_addresses;
+       }
+
+       /**
+        * Gets the email addresses for the author of a theme or a plugin.
+        *
+        * Themes: only one email.
+        * Plugins: all the plugin authors.
+        *
+        * @param WP_Comment $comment      The comment object.
+        * @param array      $comment_meta The meta values for the comment.
+        *
+        * @return array The email addresses for the author of a theme or a plugin.
+        */
+       public static function get_author_email_address( WP_Comment $comment, array $comment_meta ): array {
+               global $wpdb;
+
+               $email_addresses = array();
+               $project         = self::get_project_to_translate( $comment );
+               if ( 'wp-themes' === substr( $project->path, 0, 9 ) ) {
+                       $author = $wpdb->get_row(
+                               $wpdb->prepare(
+                                       "SELECT post_author 
+                            FROM wporg_35_posts 
+                            WHERE 
+                                post_type = 'repopackage' AND 
+                                post_name = %s
+                            ",
+                                       $project->slug
+                               )
+                       );
+                       if ( $author ) {
+                               $author            = get_user_by( 'id', $author->post_author );
+                               $email_addresses[] = $author->data->user_email;
+                       }
+               }
+               if ( 'wp-plugins' === substr( $project->path, 0, 10 ) ) {
+                       $committers = $wpdb->get_col(
+                               $wpdb->prepare(
+                                       'SELECT user FROM plugin_2_svn_access WHERE path = %s',
+                                       '/' . $project->slug
+                               )
+                       );
+                       foreach ( $committers as $user_login ) {
+                               $email_addresses[] = get_user_by( 'login', $user_login )->user_email;
+                       }
+               }
+               if ( ! ( ( 'wp-themes' === substr( $project->path, 0, 9 ) ) || ( 'wp-plugins' === substr( $project->path, 0, 10 ) ) ) ) {
+                       $email_addresses = self::$i18n_email;
+               }
+               $parent_comments        = GP_Notifications::get_parent_comments( $comment->comment_parent );
+               $emails_from_the_thread = GP_Notifications::get_commenters_email_addresses( $parent_comments );
+               // If one author has a comment in the thread or if one validator is the commenter, we don't need to inform any other validator.
+               if ( ( true !== empty( array_intersect( $email_addresses, $emails_from_the_thread ) ) ) || in_array( $comment->comment_author_email, $email_addresses, true ) ) {
+                       return array();
+               }
+               return $email_addresses;
+       }
+
+       /**
+        * Creates the email body message.
+        *
+        * @since 0.0.2
+        *
+        * @param WP_Comment $comment      The comment object.
+        * @param array|null $comment_meta The meta values for the comment.
+        *
+        * @return string|null The email body message.
+        */
+       public static function get_email_body( WP_Comment $comment, ?array $comment_meta ): ?string {
+               $project  = self::get_project_to_translate( $comment );
+               $original = self::get_original( $comment );
+               $output   = esc_html__( 'Hi:' );
+               $output  .= '<br><br>';
+               $output  .= esc_html__( 'There is a new comment in a discussion of the WordPress translation system that may be of interest to you.' );
+               $output  .= '<br>';
+               $output  .= esc_html__( 'It would be nice if you have some time to review this comment and reply to it if needed.' );
+               $output  .= '<br><br>';
+               $url      = GP_Route_Translation_Helpers::get_permalink( $project->path, $original->id );
+               $output  .= '- <strong>' . esc_html__( 'Discussion URL: ' ) . '</strong><a href="' . $url . '">' . $url . '</a><br>';
+               if ( array_key_exists( 'locale', $comment_meta ) && ( ! empty( $comment_meta['locale'][0] ) ) ) {
+                       $output .= '- <strong>' . esc_html__( 'Locale: ' ) . '</strong>' . esc_html( $comment_meta['locale'][0] ) . '<br>';
+               }
+               $output .= '- <strong>' . esc_html__( 'Original string: ' ) . '</strong>' . esc_html( $original->singular ) . '<br>';
+               if ( array_key_exists( 'translation_id', $comment_meta ) && $comment_meta['translation_id'][0] ) {
+                       $translation_id = $comment_meta['translation_id'][0];
+                       $translation    = GP::$translation->get( $translation_id );
+                       // todo: add the plurals.
+                       if ( ! is_null( $translation ) ) {
+                               $output .= '- <strong>' . esc_html__( 'Translation string: ' ) . '</strong>' . esc_html( $translation->translation_0 ) . '<br>';
+                       }
+               }
+               $output .= '- <strong>' . esc_html__( 'Comment: ' ) . '</strong>' . esc_html( $comment->comment_content ) . '</pre>';
+               $output .= '<br><br>';
+               $output .= esc_html__( 'Have a nice day' );
+               $output .= '<br><br>';
+               $output .= esc_html__( 'This is an automated message. Please, do not reply directly to this email.' );
+
+               return $output;
+       }
+
+
+       /**
+        * Gets the project the translated string belongs to.
+        *
+        * @since 0.0.2
+        *
+        * @param WP_Comment $comment The comment object.
+        *
+        * @return GP_Project The project the translated string belongs to.
+        */
+       private static function get_project_to_translate( WP_Comment $comment ): GP_Project {
+               $post_id = $comment->comment_post_ID;
+               $terms   = wp_get_object_terms( $post_id, Helper_Translation_Discussion::LINK_TAXONOMY, array( 'number' => 1 ) );
+               if ( empty( $terms ) ) {
+                       return false;
+               }
+
+               $original      = GP::$original->get( $terms[0]->slug );
+               $project_id    = $original->project_id;
+               $project       = GP::$project->get( $project_id );
+               $main_projects = self::get_main_projects();
+
+               // If the parent project is not a main project, get the parent project. We need to do this
+               // because we have 3 levels of projects. E.g. wp-plugins->akismet->stable and the PTE are
+               // assigned to the second level.
+               if ( ( ! is_null( $project->parent_project_id ) ) && ( ! in_array( $project->parent_project_id, $main_projects, true ) ) ) {
+                       $project = GP::$project->get( $project->parent_project_id );
+               }
+               return $project;
+       }
+
+       /**
+        * Gets the id of the main projects without parent projects.
+        *
+        * @since 0.0.2
+        *
+        * @return array The id of the main projects.
+        */
+       private static function get_main_projects():array {
+               global $wpdb;
+
+               $main_projects = $wpdb->get_col( "SELECT id FROM {$wpdb->gp_projects} WHERE parent_project_id IS NULL" );
+
+               return $main_projects;
+       }
+
+       /**
+        * Gets the original string that the translated string belongs to.
+        *
+        * @since 0.0.2
+        *
+        * @param WP_Comment $comment The comment object.
+        *
+        * @return GP_Thing|false The original string that the translated string belongs to.
+        */
+       private static function get_original( WP_Comment $comment ) {
+               $post_id = $comment->comment_post_ID;
+               $terms   = wp_get_object_terms( $post_id, Helper_Translation_Discussion::LINK_TAXONOMY, array( 'number' => 1 ) );
+               if ( empty( $terms ) ) {
+                       return false;
+               }
+
+               return GP::$original->get( $terms[0]->slug );
+       }
+
+       /**
+        * Gets a list with the opt-in emails.
+        *
+        * @since 0.0.2
+        *
+        * @param array $email_addresses The list of emails to be notified.
+        *
+        * @return array The list of emails with the opt-in enabled.
+        */
+       private static function get_opted_in_email_addresses( array $email_addresses ): array {
+               foreach ( $email_addresses as $email_address ) {
+                       $user            = get_user_by( 'email', $email_address );
+                       $gp_default_sort = get_user_option( 'gp_default_sort', $user->ID );
+                       if ( 'on' != gp_array_get( $gp_default_sort, 'notifications_optin', 'off' ) ) {
+                               $index = array_search( $email_address, $email_addresses, true );
+                               if ( false !== $index ) {
+                                       unset( $email_addresses[ $index ] );
+                               }
+                       }
+               }
+               return array_values( $email_addresses );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-wporg-notifications.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_htmlwpcontentpluginsgptranslationhelpersjsdiscussionjs"></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/gp-translation-helpers/js/discussion.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/js/reject-feedback.js
===================================================================
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/js/reject-feedback.js                                (rev 0)
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/js/reject-feedback.js 2022-05-11 08:47:45 UTC (rev 11837)
</ins><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,193 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/* global $gp, $gp_reject_feedback_settings, document, tb_show */
+( function( $, $gp ) {
+       $( document ).ready(
+               function() {
+                       var rowIds = [];
+                       var translationIds = [];
+                       var originalIds = [];
+                       var feedbackForm = '<details><summary class="feedback-summary">Give feedback</summary>' +
+                       '<div id="feedback-form">' +
+                       '<form>' +
+                       '<h3 class="feedback-reason-title">Reason</h3>' +
+                       '<ul class="feedback-reason-list">' +
+                       getReasonList( 'single' ) +
+                       '</ul>' +
+                       '<div class="feedback-comment">' +
+                               '<label>Comment </label>' +
+                               '<textarea name="feedback_comment"></textarea>' +
+                       '</div>' +
+                       '</form>' +
+                       '</div>' +
+                       '</details>';
+
+                       var modalFeedbackForm =
+                       '<div id="reject-feedback-form" style="display:none;">' +
+                       '<form>' +
+                       '<h3>Reason</h3>' +
+                       getReasonList( 'bulk' ) +
+                       '<div class="modal-comment">' +
+                                       '<label>Comment </label>' +
+                                       '<textarea name="modal_feedback_comment"></textarea>' +
+                       '</div>' +
+                       '<button id="modal-reject-btn" class="modal-btn">Reject</button>' +
+                       '</form>' +
+                       '</div>';
+
+                       $( 'body' ).append( modalFeedbackForm );
+
+                       // Remove click event added to <summary> by wporg-gp-customizations plugin
+                       $( $gp.editor.table ).off( 'click', 'summary' );
+
+                       $( 'button.reject' ).closest( 'dl,div.status-actions' ).prepend( feedbackForm );
+
+                       $( '#bulk-actions-toolbar-top .button, #bulk-actions-toolbar .button' ).click( function( e ) {
+                               rowIds = $( 'input:checked', $( 'table#translations th.checkbox' ) ).map( function() {
+                                       var selectedRow = $( this ).parents( 'tr.preview' );
+                                       if ( selectedRow.hasClass( 'status-current' ) ) {
+                                               return selectedRow.attr( 'row' );
+                                       }
+                                       $( this ).prop( 'checked', false );
+                                       return null;
+                               } ).get();
+
+                               rowIds.forEach( function( rowId ) {
+                                       var originalId = $gp.editor.original_id_from_row_id( rowId );
+                                       var translationId = $gp.editor.translation_id_from_row_id( rowId );
+
+                                       if ( originalId && translationId ) {
+                                               originalIds.push( originalId );
+                                               translationIds.push( translationId );
+                                       }
+                               } );
+
+                               if ( $( 'select[name="bulk[action]"]' ).val() === 'reject' ) {
+                                       e.preventDefault();
+                                       e.stopImmediatePropagation();
+                                       if ( ! translationIds.length ) {
+                                               $( 'form.filters-toolbar.bulk-actions, form#bulk-actions-toolbar-top' ).submit();
+                                               return;
+                                       }
+
+                                       // eslint-disable-next-line no-undef
+                                       tb_show( 'Reject with Feedback', '#TB_inline?inlineId=reject-feedback-form' );
+                               }
+                       } );
+
+                       $( 'body' ).on( 'click', '#modal-reject-btn', function( e ) {
+                               var comment = '';
+                               var rejectReason = [];
+                               var rejectData = {};
+                               var form = $( this ).closest( 'form' );
+
+                               form.find( 'input[name="modal_feedback_reason"]:checked' ).each(
+                                       function() {
+                                               rejectReason.push( this.value );
+                                       }
+                               );
+
+                               comment = form.find( 'textarea[name="modal_feedback_comment"]' ).val();
+
+                               if ( ( ! comment.trim().length && ! rejectReason.length ) || ( ! translationIds.length || ! originalIds.length ) ) {
+                                       $( 'form.filters-toolbar.bulk-actions, form#bulk-actions-toolbar-top' ).submit();
+                               }
+
+                               rejectData.locale_slug = $gp_reject_feedback_settings.locale_slug;
+                               rejectData.reason = rejectReason;
+                               rejectData.comment = comment;
+                               rejectData.original_id = originalIds;
+                               rejectData.translation_id = translationIds;
+                               rejectData.is_bulk_reject = true;
+                               rejectWithFeedback( rejectData );
+                               e.preventDefault();
+                       } );
+               }
+       );
+
+       $gp.editor.hooks.set_status_rejected = function() {
+               var button = $( this );
+               var rejectData = {};
+               var rejectReason = [];
+               var comment = '';
+               var div = button.closest( 'div.meta' );
+
+               div.find( 'input[name="feedback_reason"]:checked' ).each(
+                       function() {
+                               rejectReason.push( this.value );
+                       }
+               );
+
+               comment = div.find( 'textarea[name="feedback_comment"]' ).val();
+
+               if ( ! comment.trim().length && ! rejectReason.length ) {
+                       $gp.editor.set_status( button, 'rejected' );
+                       return;
+               }
+
+               rejectData.locale_slug = $gp_reject_feedback_settings.locale_slug;
+               rejectData.reason = rejectReason;
+               rejectData.comment = comment;
+               rejectData.original_id = [ $gp.editor.current.original_id ];
+               rejectData.translation_id = [ $gp.editor.current.translation_id ];
+
+               rejectWithFeedback( rejectData, button );
+       };
+
+       function rejectWithFeedback( rejectData, button ) {
+               var data = {};
+               var div = {};
+               if ( button ) {
+                       div = button.closest( 'div.meta' );
+               }
+
+               data = {
+                       action: 'reject_with_feedback',
+                       data: rejectData,
+
+                       _ajax_nonce: $gp_reject_feedback_settings.nonce,
+               };
+
+               $.ajax(
+                       {
+                               type: 'POST',
+
+                               url: $gp_reject_feedback_settings.url,
+                               data: data,
+                       }
+               ).done(
+                       function() {
+                               if ( rejectData.is_bulk_reject ) {
+                                       $( 'form.filters-toolbar.bulk-actions, form#bulk-actions-toolbar-top' ).submit();
+                               } else {
+                                       $gp.editor.set_status( button, 'rejected' );
+                                       div.find( 'input[name="feedback_reason"]' ).prop( 'checked', false );
+                                       div.find( 'textarea[name="feedback_comment"]' ).val( '' );
+                               }
+                       }
+               );
+       }
+
+       function getReasonList( displayType ) {
+               var rejectReasons = $gp_reject_feedback_settings.reject_reasons;
+
+               var rejectList = '';
+               var prefix = '';
+               var suffix = '';
+               var inputName = '';
+               if ( displayType === 'single' ) {
+                       prefix = '<li><label>';
+                       suffix = '</label></li>';
+                       inputName = 'feedback_reason';
+               } else {
+                       prefix = '<div class="modal-item"><label>';
+                       suffix = '</div></label>';
+                       inputName = 'modal_feedback_reason';
+               }
+
+               // eslint-disable-next-line vars-on-top
+               for ( var reason in rejectReasons ) {
+                       rejectList += prefix + '<input type="checkbox" name="' + inputName + '" value="' + reason + '" />' + rejectReasons[ reason ] + suffix;
+               }
+               return rejectList;
+       }
+}( jQuery, $gp )
+);
</ins></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelpersjstranslationhelpersjs"></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/gp-translation-helpers/js/translation-helpers.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/gp-translation-helpers/js/translation-helpers.js                         (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/js/translation-helpers.js   2022-05-11 08:47:45 UTC (rev 11837)
</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">+/* global $gp, window */
+$gp.translation_helpers = (
+       function( $ ) {
+               return {
+                       init: function( table, fetchNow ) {
+                               $gp.translation_helpers.table = table;
+                               $gp.translation_helpers.install_hooks();
+                               if ( fetchNow ) {
+                                       $gp.translation_helpers.fetch( false, $( '.translations' ) );
+                               }
+                       },
+                       install_hooks: function() {
+                               $( $gp.translation_helpers.table )
+                                       .on( 'beforeShow', '.editor', $gp.translation_helpers.hooks.initial_fetch )
+                                       .on( 'click', '.helpers-tabs li', $gp.translation_helpers.hooks.tab_select )
+                                       .on( 'click', 'a.comment-reply-link', $gp.translation_helpers.hooks.reply_comment_form );
+                       },
+                       initial_fetch: function( $element ) {
+                               var $helpers = $element.find( '.translation-helpers' );
+
+                               if ( $helpers.hasClass( 'loaded' ) || $helpers.hasClass( 'loading' ) ) {
+                                       return;
+                               }
+
+                               $gp.translation_helpers.fetch( false, $element );
+                       },
+                       fetch: function( which, $element ) {
+                               var $helpers;
+                               if ( $element ) {
+                                       $helpers = $element.find( '.translation-helpers' );
+                               } else {
+                                       $helpers = $( '.editor:visible, .translations' ).find( '.translation-helpers' ).first();
+                               }
+
+                               var originalId = $helpers.parent().attr( 'row' ); // eslint-disable-line vars-on-top
+                               var replytocom = $helpers.parent().attr( 'replytocom' ); // eslint-disable-line vars-on-top
+                               var requestUrl = $gp_translation_helpers_settings.th_url + originalId + '?nohc'; // eslint-disable-line
+
+                               if ( which ) {
+                                       requestUrl = requestUrl + '&helpers[]=' + which;
+                               } else {
+                                       $helpers.find( 'div.helper:not(.loaded) ' ).each( function() {
+                                               requestUrl = requestUrl + '&helpers[]=' + $( this ).data( 'helper' );
+                                       } );
+                               }
+                               requestUrl = requestUrl + '&replytocom=' + replytocom;
+
+                               if ( $helpers.find( 'div:first' ).is( ':not(.loaded)' ) ) {
+                                       $helpers.addClass( 'loading' );
+                               }
+
+                               $.getJSON(
+                                       requestUrl,
+                                       function( data ) {
+                                               $helpers.addClass( 'loaded' ).removeClass( 'loading' );
+                                               $.each( data, function( id, result ) {
+                                                       jQuery( '.helpers-tabs li[data-tab="' + id + '"]' ).find( '.count' ).text( '(' + result.count + ')' );
+                                                       $( '#' + id ).find( '.loading' ).remove();
+                                                       $( '#' + id ).find( '.async-content' ).html( result.content );
+                                               } );
+                                               $( '.helper-translation-discussion' ).find( 'form.comment-form' ).removeAttr( 'novalidate' );
+                                       }
+                               );
+                       },
+                       tab_select: function( $tab ) {
+                               var tabId = $tab.attr( 'data-tab' );
+
+                               $tab.siblings().removeClass( 'current' );
+                               $tab.parents( '.translation-helpers ' ).find( '.helper' ).removeClass( 'current' );
+
+                               $tab.addClass( 'current' );
+                               $( '#' + tabId ).addClass( 'current' );
+                       },
+                       reply_comment_form: function( $comment ) {
+                               var commentId = $comment.attr( 'data-commentid' );
+                               $( '#comment-reply-' + commentId ).toggle().find( 'textarea' ).focus();
+                               if ( 'Reply' === $comment.text() ) {
+                                       $comment.text( 'Cancel Reply' );
+                               } else {
+                                       $comment.text( 'Reply' );
+                               }
+                       },
+                       hooks: {
+                               initial_fetch: function() {
+                                       $gp.translation_helpers.initial_fetch( $( this ) );
+                                       return false;
+                               },
+                               tab_select: function() {
+                                       $gp.translation_helpers.tab_select( $( this ) );
+                                       return false;
+                               },
+                               reply_comment_form: function( event ) {
+                                       event.preventDefault();
+                                       $gp.translation_helpers.reply_comment_form( $( this ) );
+                                       return false;
+                               },
+                       },
+               };
+       }( jQuery )
+);
+
+jQuery( function( $ ) {
+       var _oldShow = $.fn.show;
+       $gp.translation_helpers.init( $( '.translations' ), true );
+       if ( typeof window.newShowFunctionAttached === 'undefined' ) {
+               window.newShowFunctionAttached = true;
+               $.fn.show = function( speed, oldCallback ) {
+                       return $( this ).each( function() {
+                               var obj = $( this ),
+                                       newCallback = function() {
+                                               if ( $.isFunction( oldCallback ) ) {
+                                                       oldCallback.apply( obj );
+                                               }
+                                       };
+
+                               obj.trigger( 'beforeShow' );
+                               _oldShow.apply( obj, [ speed, newCallback ] );
+                       } );
+               };
+       }
+} );
</ins></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsgptranslationhelperstemplatescommentsectionphp"></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/gp-translation-helpers/templates/comment-section.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/gp-translation-helpers/templates/comment-section.php                             (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/templates/comment-section.php       2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,37 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<ul class="discussion-list">
+       <?php
+               wp_list_comments(
+                       array(
+                               'style'              => 'ul',
+                               'type'               => 'comment',
+                               'callback'           => 'gth_discussion_callback',
+                               'translation_id'     => $translation_id,
+                               'original_permalink' => $original_permalink,
+                               'locale_slug'        => $locale_slug,
+                       ),
+                       $comments
+               );
+               ?>
+</ul><!-- .discussion-list -->
+<?php
+       comment_form(
+               $args = array(
+                       'title_reply'         => __( 'Discuss this string' ),
+                       /* translators: username */
+                       'title_reply_to'      => __( 'Reply to %s' ),
+                       'title_reply_before'  => '<h6 id="reply-title" class="discuss-title">',
+                       'title_reply_after'   => '</h6>',
+                       'id_form'             => 'commentform-' . $post_id,
+                       'comment_notes_after' => implode(
+                               "\n",
+                               array(
+                                       '<input type="hidden" name="comment_locale" value="' . esc_attr( $locale_slug ) . '" />',
+                                       '<input type="hidden" name="translation_id" value="' . esc_attr( $translation_id ) . '" />',
+                                       '<input type="hidden" name="redirect_to" value="' . esc_url( home_url( $_SERVER['REQUEST_URI'] ) ) . '" />',
+
+                               )
+                       ),
+               ),
+               $post_id
+       );
+       ?>
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/templates/comment-section.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_htmlwpcontentpluginsgptranslationhelperstemplatesdiscussionphp"></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/gp-translation-helpers/templates/discussion.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/gp-translation-helpers/templates/discussion.php                          (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/templates/discussion.php    2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,32 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<h1 class="discussion-heading"><?php echo esc_html( $original->singular ); ?></h1>
+
+<?php if ( $string_translation ) : ?>
+       <h2>Translation: <?php echo esc_html( $string_translation ); ?></h2>
+<?php endif; ?>
+
+<?php if ( $original_translation_permalink ) : ?>
+<a href="<?php echo esc_url( $original_translation_permalink ); ?>">View translation</a>
+<?php endif; ?>
+
+<div class="discussion-wrapper">
+       <?php if ( $number = count( $comments ) ) : ?>
+               <h4>
+                       <?php
+                       /* translators: number of comments. */
+                       printf( _n( '%s Comment', '%s Comments', $number ), number_format_i18n( $number ) );
+                       ?>
+               <?php if ( $original_translation_permalink ) : ?>
+                       <span class="comments-selector">
+                               <a href="<?php echo esc_html( $original_permalink ); ?>">Original Permalink page</a>
+                               <?php foreach ( $locales_with_comments as $locale_with_comments ) : ?>
+                                       
+                                       <a class="<?php echo esc_attr( $locale_with_comments == $locale_slug ? 'active-locale-link' : '' ); ?>" href="<?php echo esc_attr( $args['original_permalink'] . $locale_with_comments . '/default' ); ?>">
+                                               | <?php echo esc_html( $locale_with_comments ); ?>
+                                       </a>
+                               <?php endforeach; ?>
+                       </span>
+               <?php endif; ?>
+               </h4>
+       <?php endif; ?>
+       <?php gp_tmpl_load( 'comment-section', get_defined_vars(), dirname( __FILE__ ) ); ?>
+</div>
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/templates/discussion.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_htmlwpcontentpluginsgptranslationhelperstemplatesoriginalpermalinktemplatephp"></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/gp-translation-helpers/templates/original-permalink-template.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/gp-translation-helpers/templates/original-permalink-template.php                         (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/templates/original-permalink-template.php   2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,93 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<table id="translations" class="translations clear">
+               <thead>
+
+               </thead>
+               ​
+               <tbody>
+                       <tr class="editor untranslated priority-normal no-warnings no-translations" id="editor-12561869" row="12561869" style="display: table-row;">
+                               <td colspan="5">
+                                       <div class="editor-panel">
+                                               <div class="editor-panel__left">
+                                                       <div class="panel-header">
+                                                               <h3>Original</h3>
+                                                       </div>
+                                                       <div class="panel-content">
+                                                               <div class="source-string strings">
+                                                                       <div class="source-string__singular">
+                                                                               <span class="original"><?php echo esc_html( $original->singular ); ?></span>
+                                                                               <span aria-hidden="true" class="original-raw"><?php echo esc_html( $original->singular ); ?></span>
+                                                                       </div>
+                                                               </div>
+                                                               ​
+                                                               <div class="source-details">
+                                                                       <details class="source-details__references" close="">
+                                                                               <summary>Comments all
+                                                                               <?php foreach ( $locales_with_comments as $locale_with_comments ) : ?>
+                                                                                       <a class="<?php echo esc_attr( $locale_with_comments == $locale_slug ? 'active-locale-link' : '' ); ?>" href="<?php echo esc_attr( $args['original_permalink'] . $locale_with_comments . '/default1' ); ?>">
+                                                                                               | <?php echo esc_html( $locale_with_comments ); ?>
+                                                                                       </a>
+                                                                               <?php endforeach; ?>
+                                                                               </summary>
+
+                                                                               <?php gp_tmpl_load( 'comment-section', get_defined_vars(), dirname( __FILE__ ) ); ?>
+
+                                                                       </details>
+                                                               </div>
+                                                               <div class="suggestions-wrapper">
+                                                                       <details class="suggestions__other-languages initialized" data-nonce="b1ee0a8267" open="">
+                                                                               <summary>All Languages</summary>
+                                                                               <?php if ( $translations_by_locale ) : ?>
+                                                                                       <ul class="suggestions-list">
+                                                                                       <?php
+                                                                                       foreach ( $translations_by_locale as $_locale => $translation ) :
+                                                                                               ?>
+
+                                                                                               <li>
+                                                                                                       <div class="translation-suggestion with-tooltip" tabindex="0" role="button" aria-pressed="false" aria-label="Copy translation">
+                                                                                                               <span class="translation-suggestion__translation">
+                                                                                                                       <?php echo esc_html( strtoupper( $_locale ) ) . ' - ' . esc_html( $translation ); ?>
+                                                                                                               </span>
+                                                                                                       </div>
+                                                                                               </li>
+                                                                                       <?php endforeach; ?>      
+                                                                                       </ul>
+                                                                               <?php else : ?>
+                                                                                       <p class="no-suggestions">No suggestions.</p>
+                                                                               <?php endif; ?>
+                                                                       </details>
+                                                               </div>
+                                                               ​ ​
+
+                                                       </div>
+                                               </div>
+                                               ​
+                                               <div class="editor-panel__right">
+                                                       <div class="panel-header">
+                                                               <h3>Meta</h3>
+                                                       </div>
+                                                       <div class="panel-content">
+                                                               <div class="meta">
+                                                                       <dl>
+                                                                               <dt>Status:</dt>
+                                                                               <dd><?php echo esc_html( $no_of_translations ) . ' of ' . esc_html( count( $all_translation_sets ) ) . ' languages translated'; ?></dd>
+                                                                       </dl>
+                                                                       <dl>
+                                                                               <dt>Priority of the original:</dt>
+                                                                               <dd><?php echo esc_html( $priority ); ?></dd>
+                                                                       </dl>
+                                                                       <div class="source-details">
+                                                                               <details class="source-details__references">
+                                                                                       <summary>References</summary>
+                                                                                       <ul>
+                                                                                               <li><a target="_blank" href="https://plugins.trac.wordpress.org/browser/friends/trunk/templates/frontend/messages/message-form.php#L36">templates/frontend/messages/message-form.php:36</a></li>
+                                                                                       </ul>
+                                                                               </details>
+                                                                       </div>
+                                                               </div>    ​
+                                                       </div>
+                                               </div>
+                                       </div>
+                               </td>
+                       </tr> ​ 
+               </tbody>
+       </table>
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/templates/original-permalink-template.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_htmlwpcontentpluginsgptranslationhelperstemplatesoriginalpermalinkphp"></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/gp-translation-helpers/templates/original-permalink.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/gp-translation-helpers/templates/original-permalink.php                          (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/templates/original-permalink.php    2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,158 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+$breadcrumbs = array(
+       gp_project_links_from_root( $project ),
+);
+
+if ( $translation_set ) {
+       /* translators: 1: Translation set name, 2: Project name. */
+       gp_title( sprintf( __( 'Discussion &lt; %1$s &lt; %2$s &lt; GlotPress', 'glotpress' ), $translation_set->name, $project->name ) );
+       $breadcrumbs[] = $translation_set->name;
+} else {
+       /* translators: Project name. */
+       gp_title( sprintf( __( 'Discussion &lt; %s &lt; GlotPress', 'glotpress' ), $project->name ) );
+}
+
+gp_breadcrumb( $breadcrumbs );
+gp_enqueue_scripts( array( 'gp-editor', 'gp-translations-page', 'gp-translation-discussion-js' ) );
+wp_localize_script(
+       'gp-translations-page',
+       '$gp_translations_options',
+       array(
+               'sort'   => __( 'Sort', 'glotpress' ),
+               'filter' => __(
+                       'Filter',
+                       'glotpress'
+               ),
+       )
+);
+gp_enqueue_style( 'gp-discussion-css' );
+gp_tmpl_header();
+
+?>
+
+<div id="original" class="clear">
+<h1>
+       <?php
+       if ( $original->plural ) {
+               esc_html_e( 'Singular: ' );
+       }
+
+       echo esc_translation( $original->singular );
+       if ( $original->plural ) {
+               echo '<br />';
+               esc_html_e( 'Plural: ' );
+               echo esc_translation( $original->plural );
+       }
+       ?>
+</h1>
+<?php
+               $generate_permalink = function ( $translation_id ) use ( $project, $locale_slug, $translation_set_slug, $original_id ) {
+                       return GP_Route_Translation_Helpers::get_translation_permalink(
+                               $project,
+                               $locale_slug,
+                               $translation_set_slug,
+                               $original_id,
+                               $translation_id
+                       );
+               }
+               ?>
+<?php if ( $translation ) : ?>
+       <?php $translation_permalink = $generate_permalink( $translation->id ); ?>
+       <p>
+       
+               <?php echo esc_html( ucfirst( $translation->status ) ); ?> translation:
+               <?php
+               if ( ( '' == $translation->translation_1 ) && ( '' == $translation->translation_2 ) &&
+                                  ( '' == $translation->translation_3 ) && ( '' == $translation->translation_4 ) &&
+                                  ( '' == $translation->translation_5 ) ) :
+                       ?>
+                       <strong><?php echo $translation_permalink ? gp_link( $translation_permalink, esc_translation( $translation->translation_0 ) ) : esc_translation( $translation->translation_0 ); ?></strong>
+               <?php else : ?>
+                       <ul id="translation-list">
+                       <?php for ( $i = 0; $i <= 5; $i++ ) : ?>
+                               <?php if ( '' != $translation->{'translation_' . $i} ) : ?>
+                                       <li>
+                                               <?php echo $translation_permalink ? gp_link( $translation_permalink, esc_translation( $translation->{'translation_' . $i} ) ) : esc_translation( $translation->{'translation_' . $i} ); ?>
+                                       </li>
+                               <?php endif ?>
+                       <?php endfor ?>
+                       </ul>
+               <?php endif ?>
+       </p>
+<?php elseif ( $existing_translations ) : ?>
+       <?php foreach ( $existing_translations as $e ) : ?>
+               <p>
+               <?php $translation_permalink = $generate_permalink( $e->id ); ?>
+                       <?php echo esc_html( ucfirst( $e->status ) ); ?> translation:
+                       <?php
+                       if ( ( '' == $e->translation_1 ) && ( '' == $e->translation_2 ) &&
+                                          ( '' == $e->translation_3 ) && ( '' == $e->translation_4 ) &&
+                                          ( '' == $e->translation_5 ) ) :
+                               ?>
+                               <strong><?php echo $translation_permalink ? gp_link( $translation_permalink, esc_translation( $e->translation_0 ) ) : esc_translation( $e->translation_0 ); ?></strong>
+                       <?php else : ?>
+                               <ul id="translation-list">
+                                       <?php for ( $i = 0; $i <= 5; $i++ ) : ?>
+                                               <?php if ( '' != $e->{'translation_' . $i} ) : ?>
+                                                       <li>
+                                                               <?php echo $translation_permalink ? gp_link( $translation_permalink, esc_translation( $e->{'translation_' . $i} ) ) : esc_translation( $e->{'translation_' . $i} ); ?>
+                                                       </li>
+                                               <?php endif ?>
+                                       <?php endfor ?>
+                               </ul>
+                       <?php endif ?>
+               </p>
+       <?php endforeach; ?>
+<?php elseif ( $translation_set ) : ?>
+       <?php
+       $translate_url = GP_Route_Translation_Helpers::get_translation_permalink(
+               $project,
+               $locale_slug,
+               $translation_set_slug,
+               $original_id
+       );
+       ?>
+       <p>
+               <a href="<?php echo esc_url( $translate_url ); ?>"><?php esc_html_e( 'This string has no translation in this language.' ); ?></a>
+       </p>
+<?php else : ?>
+       <p>
+               <?php esc_html_e( 'This string has no translation in this language.' ); ?>
+       </p>
+<?php endif; ?>
+<div class="translations" row="<?php echo esc_attr( $row_id . ( $translation ? '-' . $translation->id : '' ) ); ?>" replytocom="<?php echo esc_attr( gp_get( 'replytocom' ) ); ?>" >
+<div class="translation-helpers">
+       <nav>
+               <ul class="helpers-tabs">
+                       <?php
+                       $is_first_class = 'current';
+                       foreach ( $sections as $section ) {
+                               // TODO: printf.
+                               echo "<li class='{$is_first_class}' data-tab='{$section['id']}'>" . esc_html( $section['title'] ) . '<span class="count">' . esc_html( $section['count'] ? ( '(' . $section['count'] . ')' ) : '' ) . '</span></li>'; // phpcs:ignore: XSS OK.
+                               $is_first_class = '';
+                       }
+                       ?>
+               </ul>
+       </nav>
+       <?php
+       $is_first_class = 'current';
+       foreach ( $sections as $section ) {
+               printf( '<div class="%s helper %s %s" id="%s" data-helper="%s">', esc_attr( $section['classname'] ), esc_attr( $is_first_class ), $section['load_inline'] ? 'loaded' : '', esc_attr( $section['id'] ), esc_attr( $section['helper'] ) );
+               if ( $section['has_async_content'] ) {
+                       echo '<div class="async-content">';
+               }
+
+               echo $section['content']; // phpcs:ignore XSS OK.
+               if ( $section['has_async_content'] ) {
+                       echo '</div>';
+               }
+               echo '</div>';
+               $is_first_class = '';
+       }
+       ?>
+</div>
+</div>
+</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/gp-translation-helpers/templates/original-permalink.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_htmlwpcontentpluginsgptranslationhelperstemplatestranslationhelpersphp"></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/gp-translation-helpers/templates/translation-helpers.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/gp-translation-helpers/templates/translation-helpers.php                         (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/templates/translation-helpers.php   2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,26 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<td colspan="3" class="translation-helpers">
+       <nav>
+               <ul class="helpers-tabs">
+                       <?php
+                       $is_first_class = 'current';
+                       foreach ( $sections as $section ) {
+                               // TODO: printf.
+                               echo "<li class='{$is_first_class}' data-tab='{$section['id']}'>" . esc_html( $section['title'] ) . '<span class="count"></span></li>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+                               $is_first_class = '';
+                       }
+                       ?>
+               </ul>
+       </nav>
+       <?php
+       $is_first_class = 'current';
+       foreach ( $sections as $section ) {
+               printf( '<div class="%s helper %s" id="%s">', esc_attr( $section['classname'] ), esc_attr( $is_first_class ), esc_attr( $section['id'] ) );
+               if ( ! $section['has_async_content'] ) {
+                       echo '<div class="async-content"></div>';
+               }
+               echo $section['content']; // phpcs:ignore XSS OK.
+               echo '</div>';
+               $is_first_class = '';
+       }
+       ?>
+</td>
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/templates/translation-helpers.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_htmlwpcontentpluginswporggpcustomizationstemplatessettingseditphp"></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-customizations/templates/settings-edit.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-customizations/templates/settings-edit.php                              (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-customizations/templates/settings-edit.php        2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,66 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * The user settings block
+ *
+ * A single table that contains all of the user settings, which is included as part of gp-templates/settings.php.
+ *
+ * @link http://glotpress.org
+ *
+ * @package GlotPress
+ * @since 2.0.0
+ */
+
+$gp_per_page = (int) get_user_option( 'gp_per_page' );
+if ( 0 === $gp_per_page ) {
+       $gp_per_page = 15;
+}
+
+$gp_default_sort = get_user_option( 'gp_default_sort' );
+if ( ! is_array( $gp_default_sort ) ) {
+       $gp_default_sort = array(
+               'by'  => 'priority',
+               'how' => 'desc',
+       );
+}
+?>
+
+<table class="form-table">
+       <tr>
+               <th><label for="per_page"><?php _e( 'Number of items per page:', 'glotpress' ); ?></label></th>
+               <td><input type="number" id="per_page" name="per_page" value="<?php echo esc_attr( $gp_per_page ); ?>"/></td>
+       </tr>
+       <tr>
+               <th><label for="default_sort[by]"><?php _e( 'Default Sort By:', 'glotpress' ); ?></label></th>
+               <td>
+                       <?php
+                       $sort_bys = wp_list_pluck( gp_get_sort_by_fields(), 'title' );
+
+                       echo gp_radio_buttons( 'default_sort[by]', $sort_bys, gp_array_get( $gp_default_sort, 'by', 'priority' ) );
+                       ?>
+               </td>
+       </tr>
+       <tr>
+               <th><label for="default_sort[how]"><?php _e( 'Default Sort Order:', 'glotpress' ); ?></label></th>
+               <td>
+                       <?php
+                       echo gp_radio_buttons(
+                               'default_sort[how]',
+                               array(
+                                       'asc'  => __( 'Ascending', 'glotpress' ),
+                                       'desc' => __( 'Descending', 'glotpress' ),
+                               ),
+                               gp_array_get( $gp_default_sort, 'how', 'desc' )
+                       );
+                       ?>
+               </td>
+       </tr>
+       <!-- Including the "notifications_optin" in the "default_sort" array is a hack.
+                If we include it in the future in the GlotPress core, it would be interesting to put
+                this value in it own option item.
+                I do this because the post values are processed in the GP_Route_Settings->settings_post,
+                and I have to modify the GlotPress core to add a new configuration item. -->
+       <tr>
+               <th><label for="default_sort[notifications_optin]"><?php _e( 'I want to receive notifications of discussions:', 'glotpress' ); ?></label></th>
+               <td><input type="checkbox" id="default_sort[notifications_optin]" name="default_sort[notifications_optin]" <?php gp_checked( 'on' == gp_array_get( $gp_default_sort, 'notifications_optin', 'off' ) ); ?> /></td>
+       </tr>
+</table>
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-customizations/templates/settings-edit.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_htmlwpcontentpluginswporggpcustomizationstemplatessettingsphp"></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-customizations/templates/settings.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-customizations/templates/settings.php                           (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-customizations/templates/settings.php     2022-05-11 08:47:45 UTC (rev 11837)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,39 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * The settings page
+ *
+ * Displays the settings page for a user.
+ *
+ * @link http://glotpress.org
+ *
+ * @package GlotPress
+ * @since 2.0.0
+ */
+
+gp_title( __( 'Your Settings &lt; GlotPress', 'glotpress' ) );
+gp_breadcrumb( array( __( 'Your Settings', 'glotpress' ) ) );
+gp_tmpl_header();
+
+$per_page = (int) get_user_option( 'gp_per_page' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+if ( 0 === $per_page ) {
+       $per_page = 15; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+}
+
+$default_sort = get_user_option( 'gp_default_sort' );
+if ( ! is_array( $default_sort ) ) {
+       $default_sort = array(
+               'by'  => 'priority',
+               'how' => 'desc',
+       );
+}
+?>
+<h2><?php _e( 'Your Settings', 'glotpress' ); ?></h2>
+<form action="" method="post">
+       <?php require_once __DIR__ . '/settings-edit.php'; ?>
+       <br>
+       <?php gp_route_nonce_field( 'update-settings_' . get_current_user_id() ); ?>
+       <input type="submit" name="submit" value="<?php esc_attr_e( 'Save Settings', 'glotpress' ); ?>">
+</form>
+
+<?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-customizations/templates/settings.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>