<!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>[2066] sites/trunk/translate.wordpress.org/includes/gp-plugins: Translate: Add a GlotPress plugin which performs basic translation checks in Javascript and warns about potential warnings before actually submitting the string.</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/2066">2066</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/2066","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>dd32</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2015-11-10 05:16:58 +0000 (Tue, 10 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: Add a GlotPress plugin which performs basic translation checks in Javascript and warns about potential warnings before actually submitting the string.
This allows for the submitter to correct the [potential] issue before submitting it into the approval queue. Many submitters don't reject their own incorrect suggestions after adding a replacement string.
See <a href="http://meta.trac.wordpress.org/ticket/1390">#1390</a></pre>
<h3>Added Paths</h3>
<ul>
<li>sites/trunk/translate.wordpress.org/includes/gp-plugins/wporg-js-warnings/</li>
<li><a href="#sitestrunktranslatewordpressorgincludesgppluginswporgjswarningswporgjswarningsjs">sites/trunk/translate.wordpress.org/includes/gp-plugins/wporg-js-warnings/wporg-js-warnings.js</a></li>
<li><a href="#sitestrunktranslatewordpressorgincludesgppluginswporgjswarningswporgjswarningsphp">sites/trunk/translate.wordpress.org/includes/gp-plugins/wporg-js-warnings/wporg-js-warnings.php</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="sitestrunktranslatewordpressorgincludesgppluginswporgjswarningswporgjswarningsjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: 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/translate.wordpress.org/includes/gp-plugins/wporg-js-warnings/wporg-js-warnings.js (rev 0)
+++ sites/trunk/translate.wordpress.org/includes/gp-plugins/wporg-js-warnings/wporg-js-warnings.js 2015-11-10 05:16:58 UTC (rev 2066)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,225 @@
</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…');
+
+ 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: $el.parent('div.textareas').prev('p.original').text() || $el.parent('div.textareas').prev('p').find('span.original').text() // Singular original, or plurals...
+ }
+ }).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…');
+ 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( /</g, '<' ).replace( />/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="sitestrunktranslatewordpressorgincludesgppluginswporgjswarningswporgjswarningsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: 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/translate.wordpress.org/includes/gp-plugins/wporg-js-warnings/wporg-js-warnings.php (rev 0)
+++ sites/trunk/translate.wordpress.org/includes/gp-plugins/wporg-js-warnings/wporg-js-warnings.php 2015-11-10 05:16:58 UTC (rev 2066)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,38 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * 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_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 editor-core GlotPress variant) this allows us to be output whenever `editor` is.
+ function replace_editor_with_our_own( $scripts ) {
+ global $wp_scripts;
+
+ $query = $wp_scripts->query( 'editor', 'registered' );
+ if ( ! $query ) {
+ return;
+ }
+
+ wp_register_script( 'editor-core', $query->src, $query->deps, $query->ver );
+ wp_localize_script( 'editor-core', $query->extra['l10n'][0], $query->extra['l10n'][1] );
+
+ wp_deregister_script( 'editor' );
+ wp_register_script( 'editor', gp_url_public_root() . 'gp-plugins/wporg-js-warnings/wporg-js-warnings.js', array( 'editor-core', 'jquery' ), '2015-11-10' );
+
+ }
+
+}
+new WPORG_JS_Translation_Warnings();
+
</ins></span></pre>
</div>
</div>
</body>
</html>