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 = ""; $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 ($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; } } $currentIdP = []; $currentMessageP = []; $currentId = ""; $currentContext = ""; $currentMessage = ""; 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 "=": 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 ($state !== _I18N_MSGID) $syntax_error("context must be defined before start of message", $lnNum); $currentContext = json_decode(substr($ln, 2)) ?? $syntax_error("malformed string", $lnNum); break; case " ": if ($state === _I18N_MSGSTR) $currentMessage .= json_decode(substr($ln, 2)) ?? $syntax_error("malformed string", $lnNum); else $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 ($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 [$metadata, $msgs, $pmsgs]; } function i18n_locale(string $locale) { global $__i18n_current_locale; $__i18n_current_locale = $locale; } function i18n_get(string $msgid, array $params = [], ?string $context = null): string { global $__i18n_current_locale, $__i18n_msg_store; $key = $msgid; if ($context !== null) $key = $context . "\x7F" . $msgid; $msg = ($__i18n_msg_store[$__i18n_current_locale] ?? [])[$msgid] ?? $key; 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 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) - 1, $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 i18n_get_current_locale(): string { global $__i18n_current_locale; return $__i18n_current_locale; } function i18n_get_available_locales(): array { global $__i18n_msg_store; return [ "en", ...array_keys($__i18n_msg_store), ]; } function i18n_get_message_store(string $locale): array { global $__i18n_msg_store, $__i18n_msg_store_plural; return [ $__i18n_msg_store[$locale] ?? (object)[], $__i18n_msg_store_plural[$locale] ?? (object)[], ]; } 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); [$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; }