<!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>[2123] sites/trunk/wordpress.org/public_html/wp-content/plugins: Translate: Convert the GP plugin "client-side JS translation validation" to a WP 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 { 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/2123">2123</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/2123","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>ocean90</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2015-11-23 22:45:21 +0000 (Mon, 23 Nov 2015)</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: Convert the GP plugin "client-side JS translation validation" to a WP plugin.

See <a href="http://meta.trac.wordpress.org/ticket/1352">#1352</a>.</pre>

<h3>Added Paths</h3>
<ul>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-js-warnings/</li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggpjswarningswporggpjswarningsjs">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-js-warnings/wporg-gp-js-warnings.js</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggpjswarningswporggpjswarningsphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-js-warnings/wporg-gp-js-warnings.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggpjswarningswporggpjswarningsjsfromrev2115sitestrunktranslatewordpressorgincludesgppluginswporgjswarningswporgjswarningsjs"></a>
<div class="copfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Copied: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-js-warnings/wporg-gp-js-warnings.js (from rev 2115, sites/trunk/translate.wordpress.org/includes/gp-plugins/wporg-js-warnings/wporg-js-warnings.js)</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-js-warnings/wporg-gp-js-warnings.js                             (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-js-warnings/wporg-gp-js-warnings.js       2015-11-23 22:45:21 UTC (rev 2123)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,229 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+$gp.wporgjswarnings = {
+       save_handler: function() {
+               var button = jQuery(this);
+
+               if (!$gp.editor.current) return;
+               var editor = $gp.editor.current;
+               button.prop('disabled', true);
+
+               $gp.notices.notice('Validating&hellip;');
+
+               var name = "translation["+editor.original_id+"][]";
+               var entries = jQuery("textarea[name='"+name+"']", editor).map(function( prop, el ) {
+                       var $el = jQuery(el);
+
+                       return {
+                               index: prop,
+                               name: name,
+                               translation: $el.val(),
+                               original: // Singular original, or plural original, or.. the single-original plural view..
+                                       $el.parent('div.textareas').prev('p.original').text() || 
+                                       $el.parent('div.textareas').prev('p').find('span.original').text() ||
+                                       jQuery( $el.parents('div.strings').find('span.original').get(prop) ).text() 
+                       }
+               }).get();
+
+               var warnings = [];
+               for ( var i in entries ) {
+                       var _warnings = $gp.wporgjswarnings.check_entry( entries[ i ] );
+                       if ( _warnings.length ) {
+                               warnings[ i ] = _warnings
+                       }
+               }
+
+               if ( warnings.length ) {
+                       var str = "The following warnings have been generated, continue?\n";
+
+                       for ( var index in warnings ) {
+                               str += "\n" + warnings[ index ] + "\n";
+                               str += "Original: " + entries[ index ].original.replace( /↵/g, '') + "\n";
+                               str += "Translation: " + entries[ index ].translation.replace( /↵/g, '' ) + "\n";
+                       }
+
+                       str += "\n[Cancel] to Edit, [OK] to Save";
+
+                       if ( ! confirm( str ) ) {
+                               $gp.notices.notice('Save aborted&hellip;');
+                               button.prop('disabled', false);
+                               return;
+                       }
+               }
+
+               // No warnings, or warnings skipped
+               $gp.editor.hooks.ok( button );
+       },
+       check_entry: function( entry ) {
+               var warnings = [];
+               for ( var i in this.warning_tests ) {
+                       var test = this.warning_tests[ i ];
+
+                       var res = test( entry );
+                       if ( typeof res == "string" ) {
+                               warnings.push( res );
+                       }
+               }
+
+               if ( warnings.length ) {
+                       return warnings;
+               }
+               return true;
+       },
+
+       overwrite: function() {
+               // Remove the standard save hook and intercept
+               jQuery($gp.editor.table).off( 'click', 'button.ok', $gp.editor.hooks.ok );
+               jQuery($gp.editor.table).on( 'click', 'button.ok', this.save_handler );
+       },
+
+       warning_tests: []
+};
+
+jQuery( function() {
+       $gp.wporgjswarnings.overwrite();
+} );
+
+// Check all placeholders exist - probably a better way than this.
+$gp.wporgjswarnings.warning_tests.push( function( entry ) {
+       var warnings = [];
+
+       var re = /%(\d+\$(?:\d+)?)?[bcdefgosuxEFGX]/g;
+       var orig_ph = entry.original.match( re );
+       var trans_ph = entry.translation.match( re );
+
+       var orig_ph_count = {}, trans_ph_count = {};
+       for ( var i in orig_ph ) {
+               var ph = orig_ph[ i ];
+               orig_ph_count[ ph ] = orig_ph_count[ ph ] || 0;
+               if ( orig_ph_count[ ph ] ) {
+                       continue;
+               }
+               for ( j in orig_ph ) {
+                       if ( ph == orig_ph[ j ] ) {
+                               orig_ph_count[ ph ]++;
+                       }
+               }
+       }
+       for ( var i in trans_ph ) {
+               var ph = trans_ph[ i ];
+               trans_ph_count[ ph ] = trans_ph_count[ ph ] || 0;
+               if ( trans_ph_count[ ph ] ) {
+                       continue;
+               }
+               for ( j in trans_ph ) {
+                       if ( ph == trans_ph[ j ] ) {
+                               trans_ph_count[ ph ]++;
+                       }
+               }
+       }
+
+       // Missing or Extra placeholder counts
+       for ( var ph in orig_ph_count ) {
+               var count = orig_ph_count[ ph ];
+               var trans_count = trans_ph_count[ ph ] || 0;
+               if ( count == trans_count ) {
+                       continue;
+               }
+               if ( count > trans_count ) {
+                       warnings.push( "Missing " + ph + " in translation" );
+               }
+               if ( trans_count > count ) {
+                       warnings.push( "Extra " + ph + " in translation" );
+               }
+       }
+       // Extra unknown placeholders
+       for ( var ph in trans_ph_count ) {
+               if ( ! orig_ph_count[ ph ] ) {
+                       warnings.push( "Extra " + ph + " in translation" );
+               }
+       }
+
+       if ( warnings.length ) {
+               return warnings.join( ", " );
+       }
+
+       return true;
+} );
+
+// Check the same leading, ending whitespace.
+$gp.wporgjswarnings.warning_tests.push( function( entry ) {
+       var warnings = [];
+       var original = entry.original.replace( /↵\n/g, "\n" ).replace( /↵/g, "\n" );
+       var translation = entry.translation.replace( /↵\n/g, "\n" ).replace( /↵/g, "\n" );
+
+       var startswith = function(string, prefix) {
+               return string.slice( 0, prefix.length ) == prefix;
+       }
+       var endswith = function(string, suffix) {
+               return string.slice( -suffix.length ) == suffix;
+       }
+
+       if ( startswith( original, "\n" ) && ! startswith( translation, "\n" ) ) {
+               warnings.push( "Translation must start on a new line" );
+       }
+       if ( ! startswith( original, "\n" ) && startswith( translation, "\n" ) ) {
+               warnings.push( "Translation must not start on a new line" );
+       }
+       if ( endswith( original, "\n" ) && ! endswith( translation, "\n" ) ) {
+               warnings.push( "Translation must end on a new line" );
+       }
+       if ( ! endswith( original, "\n" ) && endswith( translation, "\n" ) ) {
+               warnings.push( "Translation must not end on a new line" );
+       }
+
+       if ( warnings.length ) {
+               return warnings.join( ", " );
+       }
+
+       return true;
+} );
+
+// Check all HTML tags exist, and none were added.
+$gp.wporgjswarnings.warning_tests.push( function( entry ) {
+       var warnings = [];
+
+       // Convert the encoded HTML back
+       var original = entry.original.replace( /&lt;/g, '<' ).replace( /&gt;/g, '>' );
+       var translation = entry.translation;
+
+       var re = /<.+?>/g;
+       var orig_html = original.match( re ) || [];
+       var trans_html = translation.match( re ) || [];
+
+       // We don't care about title, or aria-label attributes, strip them out.
+       var attr_re = /(title|aria-label)=(["'])[^\2]+\2/g;
+       orig_html = orig_html.map( function( el ) {
+               return el.replace( attr_re, '' );
+       } );
+       trans_html = trans_html.map( function( el ) {
+               return el.replace( attr_re, '' );
+       } );
+
+       var diff = jQuery(orig_html).not(trans_html).get();
+       var rdiff = jQuery(trans_html).not(orig_html).get();
+
+       if ( rdiff.length ) {
+               warnings.push( "Unexpected HTML detected: " + rdiff.join( ", " ) );
+       }
+       if ( diff.length ) {
+               warnings.push( "Missing HTML Tags: " + diff.join( ", " ) );
+       }
+
+       if ( warnings.length ) {
+               return warnings.join( ", " );
+       }
+       return true;
+
+} );
+
+/*
+// Template for further warnings
+// Return true for "All is okay", return a string for "Are you sure you want to submit that?"
+$gp.wporgjswarnings.warning_tests.push( function( entry ) {
+       var warnings = [];
+
+       if ( warnings.length ) {
+               return warnings.join( ", " );
+       }
+       return true;
+} );
+*/
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of file
</span></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginswporggpjswarningswporggpjswarningsphpfromrev2115sitestrunktranslatewordpressorgincludesgppluginswporgjswarningswporgjswarningsphp"></a>
<div class="copfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Copied: sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-js-warnings/wporg-gp-js-warnings.php (from rev 2115, sites/trunk/translate.wordpress.org/includes/gp-plugins/wporg-js-warnings/wporg-js-warnings.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-js-warnings/wporg-gp-js-warnings.php                            (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-js-warnings/wporg-gp-js-warnings.php      2015-11-23 22:45:21 UTC (rev 2123)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,56 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Plugin name: GlotPress: Client-side JS translation validation
+ * Plugin author: dd32
+ */
+
+/**
+ * Adds client-side JS translation validation warning warnings.
+ *
+ * Not all of the warnings match exactly what is in GlotPress core, some are more specific or vague.
+ * Some of the GlotPress warnings are not duplicated into JS (rarely hit, or harder to reproduce).
+ *
+ * @TODO:
+ * - Translate the warning strings
+ * - Match warning error strings between GlotPress & this plugin?
+ *
+ * @author dd32
+ */
+class WPorg_GP_JS_Translation_Warnings {
+
+       function __construct() {
+               add_action( 'wp_print_scripts', array( $this, 'replace_editor_with_our_own' ), 100 );
+       }
+
+       /**
+        * Replace the GlotPress editor script with our own which depends on the wporg-gp-editor GlotPress variant.
+        *
+        * This allows us to be output whenever `gp-editor` is.
+        */
+       function replace_editor_with_our_own() {
+               $query = wp_scripts()->query( 'gp-editor', 'registered' );
+               if ( ! $query ) {
+                       return;
+               }
+
+               wp_register_script( 'wporg-gp-editor', $query->src, $query->deps, $query->ver );
+               if ( isset( $query->extra['l10n'] ) ) {
+                       wp_localize_script( 'wporg-gp-editor', $query->extra['l10n'][0], $query->extra['l10n'][1] );
+               }
+
+               wp_deregister_script( 'gp-editor' );
+               wp_register_script( 'gp-editor', plugins_url( '/wporg-gp-js-warnings.js', __FILE__ ), array( 'wporg-gp-editor', 'jquery' ), '2015-11-14' );
+       }
+
+}
+
+function wporg_gp_js_translation_warnings() {
+       global $wporg_gp_js_translation_warnings;
+
+       if ( ! isset( $wporg_gp_js_translation_warnings ) ) {
+               $wporg_gp_js_translation_warnings = new WPorg_GP_JS_Translation_Warnings();
+       }
+
+       return $wporg_gp_js_translation_warnings;
+}
+add_action( 'plugins_loaded', 'wporg_gp_js_translation_warnings' );
</ins></span></pre>
</div>
</div>

</body>
</html>