diff options
Diffstat (limited to 'src/application/i18n.php')
-rw-r--r-- | src/application/i18n.php | 176 |
1 files changed, 152 insertions, 24 deletions
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; } |