From fd3d969cf658475709db6a1e5090069cce51cbbd Mon Sep 17 00:00:00 2001
From: Jonas Kohl
Date: Mon, 16 Sep 2024 13:27:51 +0200
Subject: Even more i18n

---
 src/application/i18n.php | 176 ++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 152 insertions(+), 24 deletions(-)

(limited to 'src/application/i18n.php')

diff --git a/src/application/i18n.php b/src/application/i18n.php
index 2fcadab..f5b755d 100644
--- a/src/application/i18n.php
+++ b/src/application/i18n.php
@@ -1,72 +1,163 @@
 <?php
 
+use RR\Shunt\Context;
+use RR\Shunt\Parser;
+use RR\Shunt\Scanner;
+
 const MESSAGE_DIR = __DIR__ . "/messages";
 
 $__i18n_msg_store = [];
+$__i18n_msg_store_plural = [];
+$__i18n_msg_metadata = [];
 $__i18n_current_locale = null;
 
+const _I18N_MSGID = 0;
+const _I18N_MSGSTR = 1;
+const _I18N_MSGIDP = 2;
+const _I18N_MSGSTRP = 3;
+const _I18N_META = 10;
+
 function i18n_parse(string $contents, ?string $filename = null): array {
     $syntax_error = fn(string $msg, int $line): never =>
         throw new Exception("i18n syntax error: $msg (in " . ($filename ?? "unknown") . ":" . $line . ")");
     $other_error = fn(string $msg, int $line): never =>
         throw new Exception("i18n error: $msg (in " . ($filename ?? "unknown") . ":" . $line . ")");
 
+    $metadata = [];
+    
     $msgs = [];
+    $pmsgs = [];
     $lines = explode("\n", $contents);
     $currentId = "";
     $currentContext = "";
     $currentMessage = "";
-    $isInMessage = false;
+    $currentIdP = [];
+    $currentMessageP = [];
+    $currentMetadataBuffer = [];
+    $state = _I18N_MSGID;
+    
+    $processedLines = 0;
     foreach ($lines as $i => $ln) {
         $lnNum = $i + 1;
         if (trim($ln) === "")
             continue;
 
+        if ($ln === "metadata({") {
+            if ($processedLines > 0)
+                $syntax_error("metadata must come first in file", $lnNum);
+            $currentMetadataBuffer []= "{";
+            $state = _I18N_META;
+            ++$processedLines;
+            continue;
+        }
+        if ($state == _I18N_META) {
+            if ($ln === "})") {
+                $currentMetadataBuffer []= "}";
+                $state = _I18N_MSGSTR;
+                $currentMetadataBuffer = implode("\n", $currentMetadataBuffer);
+                $metadata = json_decode($currentMetadataBuffer, true);
+            } else {
+                $currentMetadataBuffer []= $ln;
+            }
+            ++$processedLines;
+            continue;
+        }
+
         switch ($ln[0]) {
             case "#":
                 continue 2;
             case ":":
-                if ($currentId !== "") {
-                    if ($currentContext !== "")
-                        $currentId = $currentContext . "\004" . $currentId;
-                    if (isset($msgs[$currentId]))
-                        $other_error("duplicate message id '$currentId'", $lnNum);
-                    $msgs[$currentId] = $currentMessage;
+                if ($state === _I18N_MSGSTRP) {
+                    // plural
+                    if (count($currentIdP) > 0) {
+                        if (isset($msgs[$currentId]))
+                            $other_error("duplicate message id '$currentId'", $lnNum);
+                        $pmsgs[$currentIdP[0]] = $currentMessageP;
+                    }
+                } else {
+                    // singular
+                    if ($currentId !== "") {
+                        if ($currentContext !== "")
+                            $currentId = $currentContext . "\x7F" . $currentId;
+                        if (isset($msgs[$currentId]))
+                            $other_error("duplicate message id '$currentId'", $lnNum);
+                        $msgs[$currentId] = $currentMessage;
+                    }
                 }
-                $currentId = json_decode(substr($ln, 2));
+                $currentIdP = [];
+                $currentMessageP = [];
+                $currentId = "";
                 $currentContext = "";
                 $currentMessage = "";
-                $isInMessage = false;
+                if ($ln === ":...") {
+                    // plural
+                    $state = _I18N_MSGIDP;
+                } else {
+                    // singular
+                    $currentId = json_decode(substr($ln, 2)) ?? $syntax_error("malformed string", $lnNum);
+                    $state = _I18N_MSGID;
+                }
+                break;
+            case "-":
+                if ($state === _I18N_MSGIDP) {
+                    $_id = json_decode(substr($ln, 2)) ?? $syntax_error("malformed string", $lnNum);
+                    if ($currentContext !== "")
+                        $_id = $currentContext . "\x7F" . $_id;
+                    $currentIdP []= $_id;
+                } elseif ($state === _I18N_MSGSTRP) {
+                    $currentMessageP []= json_decode(substr($ln, 2)) ?? $syntax_error("malformed string", $lnNum);
+                } else
+                    $syntax_error("cannot define multiple ids/messages in singular mode", $lnNum);
                 break;
             case "=":
-                $isInMessage = true;
-                $currentMessage = json_decode(substr($ln, 2));
+                if ($ln === "=...") {
+                    // plural
+                    if ($state !== _I18N_MSGIDP)
+                        $syntax_error("cannot start plural message in singular mode", $lnNum);
+                    $state = _I18N_MSGSTRP;
+                } else {
+                    // singular
+                    $state = _I18N_MSGSTR;
+                    $currentMessage = json_decode(substr($ln, 2)) ?? $syntax_error("malformed string", $lnNum);
+                }
                 break;
             case "?":
-                if ($isInMessage)
+                if ($state !== _I18N_MSGID)
                     $syntax_error("context must be defined before start of message", $lnNum);
-                $currentContext = json_decode(substr($ln, 2));
+                $currentContext = json_decode(substr($ln, 2)) ?? $syntax_error("malformed string", $lnNum);
                 break;
             case " ":
-                if ($isInMessage)
-                    $currentMessage .= json_decode(substr($ln, 2));
+                if ($state === _I18N_MSGSTR)
+                    $currentMessage .= json_decode(substr($ln, 2)) ?? $syntax_error("malformed string", $lnNum);
                 else
-                    $currentId .= json_decode(substr($ln, 2));
+                    $currentId .= json_decode(substr($ln, 2)) ?? $syntax_error("malformed string", $lnNum);
                 break;
             default:
                 $syntax_error("invalid start of line '" . $ln[0] . "' (0x" . str_pad(strtoupper(dechex(ord($ln[0]))), 2, "0", STR_PAD_LEFT) . ")", $lnNum);
                 break;
         }
+        
+        ++$processedLines;
     }
 
-    if ($currentId !== "") {
-        if ($currentContext !== "")
-            $currentId = $currentContext . "\x7F" . $currentId;
-        if (isset($msgs[$currentId]))
-            $other_error("duplicate message id '$currentId'", count($lines));
-        $msgs[$currentId] = $currentMessage;
+    if ($state === _I18N_MSGSTRP) {
+        // plural
+        if (count($currentIdP) > 0) {
+            if (isset($msgs[$currentId]))
+                $other_error("duplicate message id '$currentId'", $lnNum);
+            $pmsgs[$currentIdP[0]] = $currentMessageP;
+        }
+    } else {
+        // singular
+        if ($currentId !== "") {
+            if ($currentContext !== "")
+                $currentId = $currentContext . "\x7F" . $currentId;
+            if (isset($msgs[$currentId]))
+                $other_error("duplicate message id '$currentId'", $lnNum);
+            $msgs[$currentId] = $currentMessage;
+        }
     }
-    return $msgs;
+    return [$metadata, $msgs, $pmsgs];
 }
 
 function i18n_locale(string $locale) {
@@ -91,14 +182,51 @@ function i18n_get(string $msgid, array $params = [], ?string $context = null): s
     );
 }
 
+function i18n_get_plural(string $msgid_singular, string $msgid_plural, int $count, array $params = [], ?string $context = null): string {
+    global $__i18n_current_locale, $__i18n_msg_metadata, $__i18n_msg_store_plural;
+
+    $key = $msgid_singular;
+    if ($context !== null)
+        $key = $context . "\x7F" . $key;
+
+    $msgs = ($__i18n_msg_store_plural[$__i18n_current_locale] ?? [])[$msgid_singular] ?? null;
+    $msg = "";
+    if ($msgs === null) {
+        $msg = $count === 1 ? $msgid_singular : $msgid_plural;
+    } else {
+        $expression = (($__i18n_msg_metadata[$__i18n_current_locale] ?? [])["plural"] ?? [])["indexMapping"] ?? "n == 1 ? 0 : 1";
+        $ctx = new Context();
+        $ctx->def("ifelse", function(bool $c, $a, $b) { return $c ? $a : $b; });
+        $ctx->def("n", $count);
+        $parser = new Parser(new Scanner($expression));
+        $index = $parser->reduce($ctx);
+        $index = max(0, min(count($msgs), $index));
+        $msg = $msgs[$index];
+    }
+
+    uksort($params, fn(string $a, string $b): int => strlen($b) <=> strlen($a));
+    return str_replace(
+        array_map(fn(string $k): string => "%$k%", array_keys($params)),
+        array_values($params),
+        $msg
+    );
+}
+
 function __(string $msgid, array $params = [], ?string $context = null): string {
     return i18n_get($msgid, $params, $context);
 }
 
+function ___(string $msgid_singular, string $msgid_plural, int $count, array $params = [], ?string $context = null): string {
+    return i18n_get_plural($msgid_singular, $msgid_plural, $count, $params, $context);
+}
+
 foreach (scandir(MESSAGE_DIR) as $ent) {
     $path = MESSAGE_DIR . "/" . $ent;
     if ($ent[0] === "." || !is_file($path) || strcasecmp(pathinfo($ent, PATHINFO_EXTENSION), "msg") !== 0)
         continue;
     $lang = pathinfo($ent, PATHINFO_FILENAME);
-    $__i18n_msg_store[$lang] = i18n_parse(file_get_contents($path), $ent);
+    [$meta, $msgs, $pmsgs] = i18n_parse(file_get_contents($path), $ent);
+    $__i18n_msg_store[$lang] = $msgs;
+    $__i18n_msg_store_plural[$lang] = $pmsgs;
+    $__i18n_msg_metadata[$lang] = $meta;
 }
-- 
cgit v1.2.3