From d95382f608495bd9e413c8ae4eafda5fa6a739e8 Mon Sep 17 00:00:00 2001
From: Jared Hancock <jared@osticket.com>
Date: Wed, 9 Jul 2014 09:53:33 -0500
Subject: [PATCH] i18n: Allow multiple translations of "Open"

The software defines at least three contexts for the phrase "Open"

  * The name of a ticket queue (Open tickets)
  * A status of a ticket (Open / Closed)
  * An action to be performed (Open a ticket)

References:
(Crowdin login required)
http://i18n.osticket.com/translate/osticket-official/148/enus-de#2915
---
 include/class.i18n.php            | 14 ++++++++++--
 include/class.translation.php     |  7 +++---
 include/client/tickets.inc.php    |  2 +-
 include/staff/ticket-open.inc.php |  4 ++--
 include/staff/tickets.inc.php     |  2 +-
 scp/tickets.php                   |  7 ++++--
 setup/cli/modules/i18n.php        | 38 ++++++++++++++++++++++++-------
 setup/doc/i18n.md                 | 29 +++++++++++++++++++++--
 8 files changed, 82 insertions(+), 21 deletions(-)

diff --git a/include/class.i18n.php b/include/class.i18n.php
index 11b6f5c80..2a28487ec 100644
--- a/include/class.i18n.php
+++ b/include/class.i18n.php
@@ -334,16 +334,26 @@ class Internationalization {
             global $cfg;
             return __($msgid);
         }
-        function _SN($msgid, $plural, $count) {
+        function _NS($msgid, $plural, $count) {
             global $cfg;
         }
 
+        // Phrases with separate contexts
+        function _P($context, $msgid) {
+            return TextDomain::lookup()->getTranslation()
+                ->pgettext($context, $msgid);
+        }
+        function _NP($context, $singular, $plural, $n) {
+            return TextDomain::lookup()->getTranslation()
+                ->npgettext($context, $singular, $plural, $n);
+        }
+
         // Language-specific translations
         function _L($msgid, $locale) {
             return TextDomain::lookup()->getTranslation($locale)
                 ->translate($msgid);
         }
-        function _LN($msgid, $plural, $count, $locale) {
+        function _NL($msgid, $plural, $count, $locale) {
             return TextDomain::lookup()->getTranslation($locale)
                 ->ngettext($msgid);
         }
diff --git a/include/class.translation.php b/include/class.translation.php
index 736a12810..f4ee44662 100644
--- a/include/class.translation.php
+++ b/include/class.translation.php
@@ -3,11 +3,12 @@
     class.gettext.php
 
     This implements a `Translation` class that is loosely based on the PHP
-    gettext pure-php module.
+    gettext pure-php module. It includes some code from the project and some
+    code which is based in part at least on the PHP gettext project.
 
     This extension to the PHP gettext extension using a specially crafted MO
-    file which is a PHP serialized hash array. The file can be built using a
-    utility method in this class.
+    file which is a PHP hash array. The file can be built using a utility
+    method in this class.
 
     Jared Hancock <jared@osticket.com>
     Copyright (c)  2006-2014 osTicket
diff --git a/include/client/tickets.inc.php b/include/client/tickets.inc.php
index 3a0f48c14..a2473d27f 100644
--- a/include/client/tickets.inc.php
+++ b/include/client/tickets.inc.php
@@ -116,7 +116,7 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting
     <select name="status">
         <option value="">&mdash; <?php echo __('Any Status');?> &mdash;</option>
         <option value="open"
-            <?php echo ($status=='open')?'selected="selected"':'';?>><?php echo __('Open');?> (<?php echo $thisclient->getNumOpenTickets(); ?>)</option>
+            <?php echo ($status=='open')?'selected="selected"':'';?>><?php echo _P('ticket-status', 'Open');?> (<?php echo $thisclient->getNumOpenTickets(); ?>)</option>
         <?php
         if($thisclient->getNumClosedTickets()) {
             ?>
diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php
index 8aa896126..444362b35 100644
--- a/include/staff/ticket-open.inc.php
+++ b/include/staff/ticket-open.inc.php
@@ -373,7 +373,7 @@ if ($_POST)
         <tr>
             <td colspan=2>
                 <textarea class="richtext ifhtml draft draft-delete"
-                    placeholder="Optional internal note (recommended on assignment)"
+                    placeholder="<?php echo __('Optional internal note (recommended on assignment)'); ?>"
                     data-draft-namespace="ticket.staff.note" name="note"
                     cols="21" rows="6" style="width:80%;"
                     ><?php echo $info['note']; ?></textarea>
@@ -382,7 +382,7 @@ if ($_POST)
     </tbody>
 </table>
 <p style="text-align:center;">
-    <input type="submit" name="submit" value="<?php echo __('Open');?>">
+    <input type="submit" name="submit" value="<?php echo _P('action-button', 'Open');?>">
     <input type="reset"  name="reset"  value="<?php echo __('Reset');?>">
     <input type="button" name="cancel" value="<?php echo __('Cancel');?>" onclick="javascript:
         $('.richtext').each(function() {
diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php
index 8a427e1e1..158504928 100644
--- a/include/staff/tickets.inc.php
+++ b/include/staff/tickets.inc.php
@@ -554,7 +554,7 @@ if ($results) {
             <label for="status"><?php echo __('Status');?>:</label>
             <select id="status" name="status">
                 <option value="">&mdash; <?php echo __('Any Status');?> &mdash;</option>
-                <option value="open"><?php echo __('Open');?></option>
+                <option value="open"><?php echo _P('ticket-status', 'Open');?></option>
                 <?php
                 if(!$cfg->showAnsweredTickets()) {?>
                 <option value="answered"><?php echo __('Answered');?></option>
diff --git a/scp/tickets.php b/scp/tickets.php
index 579664d06..e3a5693fa 100644
--- a/scp/tickets.php
+++ b/scp/tickets.php
@@ -522,8 +522,11 @@ $stats= $thisstaff->getTicketsStats();
 
 //Navigation
 $nav->setTabActive('tickets');
+$open_name = _P('queue-name',
+    /* This is the name of the open ticket queue */
+    'Open');
 if($cfg->showAnsweredTickets()) {
-    $nav->addSubMenu(array('desc'=>__('Open').' ('.number_format($stats['open']+$stats['answered']).')',
+    $nav->addSubMenu(array('desc'=>$open_name.' ('.number_format($stats['open']+$stats['answered']).')',
                             'title'=>__('Open Tickets'),
                             'href'=>'tickets.php',
                             'iconclass'=>'Ticket'),
@@ -531,7 +534,7 @@ if($cfg->showAnsweredTickets()) {
 } else {
 
     if($stats) {
-        $nav->addSubMenu(array('desc'=>__('Open').' ('.number_format($stats['open']).')',
+        $nav->addSubMenu(array('desc'=>$open_name.' ('.number_format($stats['open']).')',
                                'title'=>__('Open Tickets'),
                                'href'=>'tickets.php',
                                'iconclass'=>'Ticket'),
diff --git a/setup/cli/modules/i18n.php b/setup/cli/modules/i18n.php
index 78183885e..dfa00cc71 100644
--- a/setup/cli/modules/i18n.php
+++ b/setup/cli/modules/i18n.php
@@ -259,21 +259,28 @@ class i18n_Compiler extends Module {
             }
         }
     }
-    function __read_args($tokens, $constants=1) {
+    function __read_args($tokens, $proto=false) {
         $args = array('forms'=>array());
         $arg = null;
+        $proto = $proto ?: array('forms'=>1);
 
         while (list($string,$T) = $this->__read_next_string($tokens)) {
-            if (count($args['forms']) < $constants && $string) {
+            // Add context and forms
+            if (isset($proto['context']) && !isset($args['context'])) {
+                $args['context'] = $string['form'];
+            }
+            elseif (count($args['forms']) < $proto['forms'] && $string) {
                 if (isset($string['constant']) && !$string['constant']) {
                     throw new Exception($string['form'] . ': Untranslatable string');
                 }
                 $args['forms'][] = $string['form'];
-                $args['line'] = $string['line'];
-                if (isset($string['comments']))
-                    $args['comments'] = array_merge(
-                        @$args['comments'] ?: array(), $string['comments']);
             }
+            // Add usage and comment info
+            if (!isset($args['line']) && isset($string['line']))
+                $args['line'] = $string['line'];
+            if (isset($string['comments']))
+                $args['comments'] = array_merge(
+                    @$args['comments'] ?: array(), $string['comments']);
 
             switch ($T[0]) {
             case ')':
@@ -398,6 +405,10 @@ class i18n_Compiler extends Module {
             if ($f = @$S['flags']) {
                 print "#, ".implode(', ', $f)."\n";
             }
+            if (isset($S['context'])) {
+                print "msgctxt ";
+                $this->__write_string($S['context']);
+            }
             print "msgid ";
             $this->__write_string($S['forms'][0]);
             if (count($S['forms']) == 2) {
@@ -414,7 +425,14 @@ class i18n_Compiler extends Module {
 
     function _make_pot() {
         error_reporting(E_ALL);
-        $funcs = array('__'=>1, '_S'=>1, '_N'=>2, '_SN'=>2);
+        $funcs = array(
+            '__'    => array('forms'=>1),
+            '_S'    => array('forms'=>1),
+            '_N'    => array('forms'=>2),
+            '_NS'   => array('forms'=>2),
+            '_P'    => array('context'=>1, 'forms'=>1),
+            '_NP'   => array('context'=>1, 'forms'=>2),
+        );
         $files = Test::getAllScripts();
         $strings = array();
         foreach ($files as $f) {
@@ -436,6 +454,8 @@ class i18n_Compiler extends Module {
         $primary = $forms[0];
         // Normalize the $primary string
         $primary = preg_replace(array("`\\\(['$])`", '`(?<!\\\)"`'), array("$1", '\"'), $primary);
+        if (isset($call['context']))
+            $primary = $call['context'] . "\x04" . $primary;
         if (!isset($strings[$primary])) {
             $strings[$primary] = array('forms' => $forms);
         }
@@ -447,11 +467,13 @@ class i18n_Compiler extends Module {
             $E['flags'] = array_unique(array_merge(@$E['flags'] ?: array(), $call['flags']));
         if (isset($call['comments']))
             $E['comments'] = array_merge(@$E['comments'] ?: array(), $call['comments']);
+        if (isset($call['context']))
+            $E['context'] = $call['context'];
     }
 
     function __getAllJsPhrases() {
         $strings = array();
-        $funcs = array('__'=>1);
+        $funcs = array('__'=>array('forms'=>1));
         foreach (glob_recursive(ROOT_DIR . "*.js") as $s) {
             $script = file_get_contents($s);
             $s = str_replace(ROOT_DIR, '', $s);
diff --git a/setup/doc/i18n.md b/setup/doc/i18n.md
index 11cf18a59..210489667 100644
--- a/setup/doc/i18n.md
+++ b/setup/doc/i18n.md
@@ -44,13 +44,18 @@ Use a few function calls to get your text localized:
   * `_N('string', 'strings', n)` localize a string with plural alternatives
     to the current user locale preference.
   * `_S('string')` localize the string to the system primary language
-  * `_SN('string', 'strings', n)` localize a string with plural alternatives
+  * `_NS('string', 'strings', n)` localize a string with plural alternatives
     to the system primary language.
   * `_L('string', locale)` localize a string to a named locale. This is
     useful in a ticket thread, for instance, which may have a language
     preference separate from both the user and the system primary language
-  * `_LN('string', 'strings', n, locale)` localize a string with plural
+  * `_NL('string', 'strings', n, locale)` localize a string with plural
     alternatives to a specific locale.
+  * `_P('context', 'string')` localize a string which may have the same
+    source text as another string but have a different context or usage. An
+    example would be `open` used as a noun, adjective, and verb.
+  * `_NP('context', 'string', 'strings', n)` localize a string with plural
+    alternatives which may have different contexts in the source language.
 
 In some cases, it is not possible to use a function to translate your
 string. For instance, if it is used a as a class constant or default value
@@ -69,6 +74,11 @@ set something like this up:
 In this case the localized version of the class variable is translated when
 it is used — not when it is defined.
 
+The `* trans *` text in comment is significant. Both the asterisks and the
+term `trans` are used to signify that the phrase should be made
+translatable. The comment must also be placed immedately before or after the
+string without any other PHP symbols in between (like the semi-colon).
+
 ### Adding context to your strings
 
 Your text may be ambiguous or unclear when it is viewed outside the context
@@ -81,6 +91,21 @@ in the translation template. For instance
     print __('Localized' /* These comments are exported */);
 ```
 
+All types of PHP comments are supported. They must be placed inside the
+parentheses of the localization call. If using single-line comments, use
+multiple lines if necessary to define the call so that your single-line
+comments can be used. For instance:
+
+```php
+    print sprintf(__(
+        // Tokens will be <a> of <b> <object(s)>
+        '%1$d of %2$d %3$s'
+        ),
+        $a, $b,
+        _N('object', 'objects', $b)
+    );
+```
+
 ### Building POT file for translations
 
 Use the command line to compile the POT file to standard out
-- 
GitLab