• Welcome to the Chevereto user community!

    Here users from all over the world gather around to learn the latest about Chevereto and contribute with ideas to improve the software.

    Please keep in mind:

    • 😌 This community is user driven. Be polite with other users.
    • 👉 Is required to purchase a Chevereto license to participate in this community (doesn't apply to Pre-sales).
    • 💸 Purchase a Pro Subscription to get access to active software support and faster ticket response times.
  • Chevereto Support CLST

    Support response

    Support checklist

[v3.x] php-gettext.php library ignores "Plural-Forms" header in .mo files

Denpa

Chevereto Member
edit. Sorry for posting in wrong section, mean to post in support, just clicked new thread button in wrong window.

This is just a bug report in case it's not a known issue.

As thread name states php-gettext.php ignores Plural-Forms header in gettext .mo files and instead uses really simple and for some languages improper logic to choose a plural form from translation file.

For example in Russian, plural form must be determined by this ternary operation(as indicated here):
Code:
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"

So we'll need to have
Code:
msgstr[0] for 1, 21, 31...
msgstr[1] for 2, 3, 4, 22, 23, 24, 32, 33, 34...
msgstr[2] for 0, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 25, 26, 27, 28, 29, 30, 35, 36...

instead of
Code:
msgstr[0] for 1
msgstr[1] for 2
msgstr[2] for any other number
as determined by this part of php-gettext library:

/app/vendor/php-gettext.php:244
Code:
if ($count <= 0 || count($translation) < $count) {
   $count = count($translation);
}
return $translation[$count - 1];

Although in whole scheme of things it's really minor issue, I think it's worth looking at it, because otherwise there's really no point in different plural forms for some languages.

And if it is a known issue, sorry for wasting your time.
 
Last edited:
Tried to do something with php-gettext.php to make it properly evaluate plural expression with
create_function(). ( I know it's not optimal, but it's still better than pure eval() )

It seems to be working fine with all languages I tried, but needs a better way to get header from .mo file.
Sadly I run out of time for today.

I'm professional piano player and not a developer, so it may not be an optimal solution.
But I hope it'll help anyway.

PHP:
/**
* This code here was a proof of concept only.
* DO NOT USE IN PRODUCTION!
* Use final php-gettext.php from message below instead.
*/

  /**
  * Horrible thing.
  * Strictly for testing until I figure out .mo file format.
  */
  private function getHeaders() {
     $headers = array();
     $translation = file_get_contents( $this->mofile );
     $lines = explode("\n", $translation);
     foreach($lines as $line) {
        $parts = explode(':', $line, 2);
        if (!isset($parts[1])) continue;
        $headers[trim($parts[0])] = trim($parts[1]);
     }
     return $headers;
  }

   /**
   * Parse a number of forms and expression value from headers
   *
   * @return Array with number of forms and expression
   */
   private function parsePlurals() {
     $headers = $this->getHeaders();

     if (preg_match('/^\s*nplurals\s*=\s*(\d+)\s*;\s+plural\s*=\s*(.+)$/', $headers['Plural-Forms'], $matches)) {
        $nplurals = (int)$matches[1];
        $expression = trim( $this->parenthesizePluralExpression($matches[2]) );
        return array($nplurals, $expression);
     } else {
        return array(2, 'n != 1'); // Fallback to two forms for 'one' and 'many'.
     }
   }

   /**
   * Create function to evaluate plural expression and call it
   *
   * @param Integer  $count  The number for plural form.
   *
   * @return Integer  Index of plural form for translation array
   */
   private function selectPluralForm($count) {
      list( $nplurals, $expression ) = $this->parsePlurals();

      $expression = str_replace('n', '$n', $expression);
      $func = "\$index = (int)($expression); return (\$index < $nplurals)? \$index : $nplurals - 1;";
      $_selectPluralForm = create_function('$n', $func);

      return $_selectPluralForm($count);
   }

   /**
   * Add parentheses to the inner parts of ternary operators.
   * Needed because of the way PHP evaluates them.
   *
   * @param String  $expression  Plural form expression
   *
   * @return String  $res  Plural form expression with parenthesis
   */
   private function parenthesizePluralExpression($expression) {
      $expression .= ';';
      $res = '';
      $depth = 0;
      for ($i = 0; $i < strlen($expression); ++$i) {
         $char = $expression[$i];
         switch ($char) {
            case '?':
               $res .= ' ? (';
               $depth++;
               break;
            case ':':
               $res .= ') : (';
               break;
            case ';':
               $res .= str_repeat(')', $depth) . ';';
               $depth= 0;
               break;
            default:
               $res .= $char;
         }
      }
      return rtrim($res, ';');
   }

  // Modified function from original Gettext_PHP class
  public function ngettext($msg, $msg_plural, $count) {
     if (!$this->parsed) {
        $this->parse();
     }

     $msg = (string) $msg;

     if (array_key_exists($msg, $this->translationTable)) {
        $translation = $this->translationTable[$msg];
        return $translation[ $this->selectPluralForm($count) ];
     }

     /* not found, handle count */
     if (1 == $count) {
        return $msg;
     } else {
        return $msg_plural;
     }
  }
 
Last edited:
edit.
Updated it a little, cause there's really no need to create plural form function on every(!) ngettext call, what was I thinking?!
Now it's final and efficient, I promise.


OK.
I think I got it done.

Modified /app/vendor/php-gettext.php attached to this message and on http://pastebin.com/Y3Qrca1V.

Header will now be parsed on parse() function call to avoid reading translation file twice and then it'll be put in array form into protected variable $headerTable for future use by parsePlurals() function.

Modified lines are
47-48($headerTable and $_selectPluralForm declaration), 143-229(added functions), 268(header parsing), 336

I will test this thing on my server for a while but it should be pretty safe and consistent.

If you find it useful, you're welcome to use it Rodolfo.
It'll be more important, as more languages are added, but even now, it matters for Russian, Polish and Arabic translations.
 

Attachments

  • php-gettext-final.zip
    3.7 KB · Views: 2
Last edited:
Back
Top