From 778f70d2d53f582cecb03f0ad3445a55ff5e2980 Mon Sep 17 00:00:00 2001
From: Jared Hancock <jared@osticket.com>
Date: Wed, 22 Apr 2015 10:49:40 -0500
Subject: [PATCH] i18n: Add language and spell check hints

This patch adds Content-Language and <link rel="alternate"> information to
the client interface for better integration with search engines. It also
adds @lang attribute to the <html> element and various input, textarea, and
richtext boxes for better spell check integration and accuracy.
---
 include/ajax.config.php                       |  4 +--
 include/class.i18n.php                        |  9 ++++++
 include/client/header.inc.php                 | 29 +++++++++++++++++--
 include/staff/header.inc.php                  |  7 +++--
 .../staff/templates/content-manage.tmpl.php   |  6 ++--
 include/staff/ticket-view.inc.php             |  8 ++---
 js/redactor-osticket.js                       |  6 ++++
 scp/js/jquery.translatable.js                 |  3 ++
 scp/js/scp.js                                 | 10 +++++++
 9 files changed, 70 insertions(+), 12 deletions(-)

diff --git a/include/ajax.config.php b/include/ajax.config.php
index c263bd4a2..a4e11bf22 100644
--- a/include/ajax.config.php
+++ b/include/ajax.config.php
@@ -46,7 +46,7 @@ class ConfigAjaxAPI extends AjaxController {
               'has_rtl'         => $rtl,
               'lang_flag'       => strtolower($info['flag'] ?: $locale ?: $sl),
               'primary_lang_flag' => strtolower($primary_info['flag'] ?: $primary_locale ?: $primary_sl),
-              'primary_language' => $primary,
+              'primary_language' => Internationalization::rfc1766($primary),
               'secondary_languages' => $cfg->getSecondaryLanguages(),
               'page_size'       => $thisstaff->getPageLimit(),
         );
@@ -70,7 +70,7 @@ class ConfigAjaxAPI extends AjaxController {
             'lang'            => $lang,
             'short_lang'      => $sl,
             'has_rtl'         => $rtl,
-            'primary_language' => $cfg->getPrimaryLanguage(),
+            'primary_language' => Internationalization::rfc1766($cfg->getPrimaryLanguage()),
             'secondary_languages' => $cfg->getSecondaryLanguages(),
         );
 
diff --git a/include/class.i18n.php b/include/class.i18n.php
index 94f6b679c..27486ba9b 100644
--- a/include/class.i18n.php
+++ b/include/class.i18n.php
@@ -408,6 +408,15 @@ class Internationalization {
         return $locale;
     }
 
+    static function rfc1766($what) {
+        if (is_array($what))
+            return array_map(array(get_called_class(), 'rfc1766'), $what);
+
+        $lr = explode('_', $what);
+        if (isset($lr[1]))
+            $lr[1] = strtoupper($lr[1]);
+        return implode('-', $lr);
+    }
 
     static function getTtfFonts() {
         if (!class_exists('Phar'))
diff --git a/include/client/header.inc.php b/include/client/header.inc.php
index db6e4006c..c49e89639 100644
--- a/include/client/header.inc.php
+++ b/include/client/header.inc.php
@@ -8,11 +8,17 @@ $signout_url = ROOT_PATH . "logout.php?auth=".$ost->getLinkToken();
 header("Content-Type: text/html; charset=UTF-8");
 ?>
 <!DOCTYPE html>
-<html <?php
+<html<?php
 if (($lang = Internationalization::getCurrentLanguage())
         && ($info = Internationalization::getLanguageInfo($lang))
         && (@$info['direction'] == 'rtl'))
-    echo 'dir="rtl" class="rtl"';
+    echo ' dir="rtl" class="rtl"';
+if ($lang) {
+    $langs = array_unique(array($lang, $cfg->getPrimaryLanguage()));
+    $langs = Internationalization::rfc1766($langs);
+    echo ' lang="' . $lang . '"';
+    header("Content-Language: ".implode(', ', $langs));
+}
 ?>>
 <head>
     <meta charset="utf-8">
@@ -48,6 +54,25 @@ if (($lang = Internationalization::getCurrentLanguage())
     if($ost && ($headers=$ost->getExtraHeaders())) {
         echo "\n\t".implode("\n\t", $headers)."\n";
     }
+
+    // Offer alternate links for search engines
+    // @see https://support.google.com/webmasters/answer/189077?hl=en
+    if (($all_langs = Internationalization::getConfiguredSystemLanguages())
+        && (count($all_langs) > 1)
+    ) {
+        $langs = Internationalization::rfc1766(array_keys($all_langs));
+        $qs = array();
+        parse_str($_SERVER['QUERY_STRING'], $qs);
+        foreach ($langs as $L) {
+            $qs['lang'] = $L; ?>
+        <link rel="alternate" href="//<?php echo $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; ?>?<?php
+            echo http_build_query($qs); ?>" hreflang="<?php echo $L; ?>" />
+<?php
+        } ?>
+        <link rel="alternate" href="//<?php echo $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; ?>"
+            hreflang="x-default";
+<?php
+    }
     ?>
 </head>
 <body>
diff --git a/include/staff/header.inc.php b/include/staff/header.inc.php
index 0945a9164..0e7bf469c 100644
--- a/include/staff/header.inc.php
+++ b/include/staff/header.inc.php
@@ -2,11 +2,14 @@
 header("Content-Type: text/html; charset=UTF-8");
 if (!isset($_SERVER['HTTP_X_PJAX'])) { ?>
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
-<html <?php
+<html<?php
 if (($lang = Internationalization::getCurrentLanguage())
         && ($info = Internationalization::getLanguageInfo($lang))
         && (@$info['direction'] == 'rtl'))
-    echo 'dir="rtl" class="rtl"';
+    echo ' dir="rtl" class="rtl"';
+if ($lang) {
+    echo ' lang="' . Internationalization::rfc1766($lang) . '"';
+}
 ?>>
 <head>
     <meta http-equiv="content-type" content="text/html; charset=UTF-8">
diff --git a/include/staff/templates/content-manage.tmpl.php b/include/staff/templates/content-manage.tmpl.php
index 3fa4e8fcb..e90b8ff87 100644
--- a/include/staff/templates/content-manage.tmpl.php
+++ b/include/staff/templates/content-manage.tmpl.php
@@ -27,7 +27,8 @@ if (count($langs) > 1) { ?>
         class="tab_content left-tabs" style="padding:0" lang="<?php echo $cfg->getPrimaryLanguage(); ?>">
     <div class="error"><?php echo $errors['name']; ?></div>
     <input type="text" style="width: 100%; font-size: 14pt" name="name" value="<?php
-        echo Format::htmlchars($info['title']); ?>" />
+    echo Format::htmlchars($info['title']); ?>" spellcheck="true"
+        lang="<?php echo $cfg->getPrimaryLanguage(); ?>" />
     <div style="margin-top: 5px">
     <div class="error"><?php echo $errors['body']; ?></div>
     <textarea class="richtext no-bar" name="body"
@@ -46,7 +47,8 @@ if (count($langs) > 1) { ?>
     <input type="text" style="width: 100%; font-size: 14pt"
         name="trans[<?php echo $tag; ?>][title]" value="<?php
         echo Format::htmlchars($trans['title']); ?>"
-        placeholder="<?php echo __('Title'); ?>" />
+        placeholder="<?php echo __('Title'); ?>"  spellcheck="true"
+        lang="<?php echo $tag; ?>" />
     <div style="margin-top: 5px">
     <textarea class="richtext no-bar" data-direction=<?php echo $nfo['direction']; ?>
         data-root-context="<?php echo $content->getType(); ?>"
diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php
index 35fb34aca..1bd831a38 100644
--- a/include/staff/ticket-view.inc.php
+++ b/include/staff/ticket-view.inc.php
@@ -433,7 +433,7 @@ $tcount = $ticket->getThreadEntries($types)->count();
     </ul>
     <?php
     if ($role->hasPerm(TicketModel::PERM_REPLY)) { ?>
-    <form id="reply" class="tab_content" action="tickets.php?id=<?php
+    <form id="reply" class="tab_content spellcheck" action="tickets.php?id=<?php
         echo $ticket->getId(); ?>" name="reply" method="post" enctype="multipart/form-data">
         <?php csrf_token(); ?>
         <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>">
@@ -622,7 +622,7 @@ $tcount = $ticket->getThreadEntries($types)->count();
     </form>
     <?php
     } ?>
-    <form id="note" class="hidden tab_content" action="tickets.php?id=<?php
+    <form id="note" class="hidden tab_content spellcheck" action="tickets.php?id=<?php
         echo $ticket->getId(); ?>#note" name="note" method="post" enctype="multipart/form-data">
         <?php csrf_token(); ?>
         <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>">
@@ -705,7 +705,7 @@ $tcount = $ticket->getThreadEntries($types)->count();
    </form>
     <?php
     if ($role->hasPerm(TicketModel::PERM_TRANSFER)) { ?>
-    <form id="transfer" class="hidden tab_content" action="tickets.php?id=<?php
+    <form id="transfer" class="hidden tab_content spellcheck" action="tickets.php?id=<?php
         echo $ticket->getId(); ?>#transfer" name="transfer" method="post" enctype="multipart/form-data">
         <?php csrf_token(); ?>
         <input type="hidden" name="ticket_id" value="<?php echo $ticket->getId(); ?>">
@@ -766,7 +766,7 @@ $tcount = $ticket->getThreadEntries($types)->count();
     } ?>
     <?php
     if ($role->hasPerm(TicketModel::PERM_ASSIGN)) { ?>
-    <form id="assign" class="hidden tab_content" action="tickets.php?id=<?php
+    <form id="assign" class="hidden tab_content spellcheck" action="tickets.php?id=<?php
          echo $ticket->getId(); ?>#assign" name="assign" method="post" enctype="multipart/form-data">
         <?php csrf_token(); ?>
         <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>">
diff --git a/js/redactor-osticket.js b/js/redactor-osticket.js
index 23b1ee76a..19ff52a6a 100644
--- a/js/redactor-osticket.js
+++ b/js/redactor-osticket.js
@@ -268,6 +268,12 @@ $(function() {
                 'tabFocus': false,
                 'toolbarFixedBox': true,
                 'focusCallback': function() { this.$box.addClass('no-pjax'); },
+                'initCallback': function() {
+                    this.$editor.attr('spellcheck', 'true');
+                    var lang = this.$editor.closest('[lang]').attr('lang');
+                    if (lang)
+                        this.$editor.attr('lang', lang);
+                },
                 'linkSize': 100000,
                 'definedLinks': 'ajax.php/config/links'
             }, options||{});
diff --git a/scp/js/jquery.translatable.js b/scp/js/jquery.translatable.js
index 08a28ae3c..08f754a1a 100644
--- a/scp/js/jquery.translatable.js
+++ b/scp/js/jquery.translatable.js
@@ -60,6 +60,7 @@
         .focus($.proxy(function() { this.addClass('focus'); }, this.$container))
         .blur($.proxy(function() { this.removeClass('focus'); }, this.$container));
       getConfig().then($.proxy(function(c) {
+        this.attr({'spellcheck': 'true', 'lang': c.primary_language})
         $('<span class="flag"></span>')
           .addClass('flag-' + c.primary_lang_flag)
           .insertAfter(this);
@@ -113,6 +114,8 @@
           .text(info.name)
           .prepend($('<span>').addClass('flag flag-'+info.flag))
           .append($('<input type="text" data-lang="'+lang+'">')
+            .attr('lang', lang)
+            .attr('spellcheck', 'true')
             .attr('dir', info.direction || 'ltr')
             .on('change keydown', $.proxy(this.showCommit, this))
             .val(text)
diff --git a/scp/js/scp.js b/scp/js/scp.js
index 24be12c76..502b7f061 100644
--- a/scp/js/scp.js
+++ b/scp/js/scp.js
@@ -563,6 +563,16 @@ $(document).keydown(function(e) {
     }
 });
 
+
+$(document).on('focus', 'form.spellcheck textarea, form.spellcheck input[type=text]', function() {
+  var $this = $(this);
+  if ($this.attr('lang') !== undefined)
+    return;
+  var lang = $(this).closest('[lang]').attr('lang');
+  if (lang)
+    $(this).attr({'spellcheck':'true', 'lang': lang});
+});
+
 $.toggleOverlay = function (show) {
   if (typeof(show) === 'undefined') {
     return $.toggleOverlay(!$('#overlay').is(':visible'));
-- 
GitLab