Add support for base text formats for Translatable

fixes #5342 and various other issues related to formatting codes in user input
This commit is contained in:
Dylan K. Taylor 2025-06-14 16:45:23 +01:00
parent 48b80ecf78
commit c4fb8832fe
No known key found for this signature in database
GPG Key ID: 8927471A91CAFD3D
16 changed files with 65 additions and 33 deletions

View File

@ -123,7 +123,7 @@ abstract class Command{
}
if($this->permissionMessage === null){
$target->sendMessage(KnownTranslationFactory::pocketmine_command_error_permission($this->name)->prefix(TextFormat::RED));
$target->sendMessage(KnownTranslationFactory::pocketmine_command_error_permission($this->name)->baseTextFormat(TextFormat::RED));
}elseif($this->permissionMessage !== ""){
$target->sendMessage(str_replace("<permission>", $permission ?? implode(";", $this->permission), $this->permissionMessage));
}
@ -237,7 +237,7 @@ abstract class Command{
public static function broadcastCommandMessage(CommandSender $source, Translatable|string $message, bool $sendToSource = true) : void{
$users = $source->getServer()->getBroadcastChannelSubscribers(Server::BROADCAST_CHANNEL_ADMINISTRATIVE);
$result = KnownTranslationFactory::chat_type_admin($source->getName(), $message);
$colored = $result->prefix(TextFormat::GRAY . TextFormat::ITALIC);
$colored = $result->baseTextFormat(TextFormat::GRAY . TextFormat::ITALIC);
if($sendToSource){
$source->sendMessage($message);

View File

@ -107,7 +107,7 @@ class FormattedCommandAlias extends Command{
$timings->stopTiming();
}
}else{
$sender->sendMessage($sender->getLanguage()->translate(KnownTranslationFactory::pocketmine_command_notFound($commandLabel, "/help")->prefix(TextFormat::RED)));
$sender->sendMessage($sender->getLanguage()->translate(KnownTranslationFactory::pocketmine_command_notFound($commandLabel, "/help")->baseTextFormat(TextFormat::RED)));
//to match the behaviour of SimpleCommandMap::dispatch()
//this shouldn't normally happen, but might happen if the command was unregistered or modified after

View File

@ -226,7 +226,7 @@ class SimpleCommandMap implements CommandMap{
return true;
}
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_notFound($sentCommandLabel ?? "", "/help")->prefix(TextFormat::RED));
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_notFound($sentCommandLabel ?? "", "/help")->baseTextFormat(TextFormat::RED));
return false;
}

View File

@ -69,7 +69,7 @@ class ClearCommand extends VanillaCommand{
}
}catch(LegacyStringToItemParserException $e){
//vanilla checks this at argument parsing layer, can't come up with a better alternative
$sender->sendMessage(KnownTranslationFactory::commands_give_item_notFound($args[1])->prefix(TextFormat::RED));
$sender->sendMessage(KnownTranslationFactory::commands_give_item_notFound($args[1])->baseTextFormat(TextFormat::RED));
return true;
}
}
@ -90,7 +90,7 @@ class ClearCommand extends VanillaCommand{
if($count > 0){
$sender->sendMessage(KnownTranslationFactory::commands_clear_testing($target->getName(), (string) $count));
}else{
$sender->sendMessage(KnownTranslationFactory::commands_clear_failure_no_items($target->getName())->prefix(TextFormat::RED));
$sender->sendMessage(KnownTranslationFactory::commands_clear_failure_no_items($target->getName())->baseTextFormat(TextFormat::RED));
}
return true;
@ -132,7 +132,7 @@ class ClearCommand extends VanillaCommand{
if($clearedCount > 0){
Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_clear_success($target->getName(), (string) $clearedCount));
}else{
$sender->sendMessage(KnownTranslationFactory::commands_clear_failure_no_items($target->getName())->prefix(TextFormat::RED));
$sender->sendMessage(KnownTranslationFactory::commands_clear_failure_no_items($target->getName())->baseTextFormat(TextFormat::RED));
}
return true;

View File

@ -68,7 +68,7 @@ class EffectCommand extends VanillaCommand{
$effect = StringToEffectParser::getInstance()->parse($args[1]);
if($effect === null){
$sender->sendMessage(KnownTranslationFactory::commands_effect_notFound($args[1])->prefix(TextFormat::RED));
$sender->sendMessage(KnownTranslationFactory::commands_effect_notFound($args[1])->baseTextFormat(TextFormat::RED));
return true;
}

View File

@ -66,7 +66,7 @@ class GiveCommand extends VanillaCommand{
try{
$item = StringToItemParser::getInstance()->parse($args[1]) ?? LegacyStringToItemParser::getInstance()->parse($args[1]);
}catch(LegacyStringToItemParserException $e){
$sender->sendMessage(KnownTranslationFactory::commands_give_item_notFound($args[1])->prefix(TextFormat::RED));
$sender->sendMessage(KnownTranslationFactory::commands_give_item_notFound($args[1])->baseTextFormat(TextFormat::RED));
return true;
}
@ -101,7 +101,7 @@ class GiveCommand extends VanillaCommand{
$player->getInventory()->addItem($item);
Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_give_success(
$item->getName() . " (" . $args[1] . ")",
$item->getName() . TextFormat::RESET . " (" . $args[1] . ")",
(string) $item->getCount(),
$player->getName()
));

View File

@ -105,22 +105,22 @@ class HelpCommand extends VanillaCommand{
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_header($cmd->getLabel())
->format(TextFormat::YELLOW . "--------- " . TextFormat::RESET, TextFormat::YELLOW . " ---------"));
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_description(TextFormat::RESET . $descriptionString)
->prefix(TextFormat::GOLD));
->baseTextFormat(TextFormat::GOLD));
$usage = $cmd->getUsage();
$usageString = $usage instanceof Translatable ? $lang->translate($usage) : $usage;
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_usage(TextFormat::RESET . implode("\n" . TextFormat::RESET, explode("\n", $usageString, limit: PHP_INT_MAX)))
->prefix(TextFormat::GOLD));
->baseTextFormat(TextFormat::GOLD));
$aliases = $cmd->getAliases();
sort($aliases, SORT_NATURAL);
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_aliases(TextFormat::RESET . implode(", ", $aliases))
->prefix(TextFormat::GOLD));
->baseTextFormat(TextFormat::GOLD));
return true;
}
}
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_notFound($commandName, "/help")->prefix(TextFormat::RED));
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_notFound($commandName, "/help")->baseTextFormat(TextFormat::RED));
return true;
}

View File

@ -114,7 +114,7 @@ class ParticleCommand extends VanillaCommand{
$particle = $this->getParticle($name, $data);
if($particle === null){
$sender->sendMessage(KnownTranslationFactory::commands_particle_notFound($name)->prefix(TextFormat::RED));
$sender->sendMessage(KnownTranslationFactory::commands_particle_notFound($name)->baseTextFormat(TextFormat::RED));
return true;
}

View File

@ -52,7 +52,7 @@ class SayCommand extends VanillaCommand{
$sender->getServer()->broadcastMessage(KnownTranslationFactory::chat_type_announcement(
$sender instanceof Player ? $sender->getDisplayName() : ($sender instanceof ConsoleCommandSender ? "Server" : $sender->getName()),
implode(" ", $args)
)->prefix(TextFormat::LIGHT_PURPLE));
)->baseTextFormat(TextFormat::LIGHT_PURPLE));
return true;
}
}

View File

@ -60,9 +60,9 @@ class TellCommand extends VanillaCommand{
if($player instanceof Player){
$message = implode(" ", $args);
$sender->sendMessage(KnownTranslationFactory::commands_message_display_outgoing($player->getDisplayName(), $message)->prefix(TextFormat::GRAY . TextFormat::ITALIC));
$sender->sendMessage(KnownTranslationFactory::commands_message_display_outgoing($player->getDisplayName(), $message)->baseTextFormat(TextFormat::GRAY . TextFormat::ITALIC));
$name = $sender instanceof Player ? $sender->getDisplayName() : $sender->getName();
$player->sendMessage(KnownTranslationFactory::commands_message_display_incoming($name, $message)->prefix(TextFormat::GRAY . TextFormat::ITALIC));
$player->sendMessage(KnownTranslationFactory::commands_message_display_incoming($name, $message)->baseTextFormat(TextFormat::GRAY . TextFormat::ITALIC));
Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_message_display_outgoing($player->getDisplayName(), $message), false);
}else{
$sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound());

View File

@ -99,11 +99,11 @@ abstract class VanillaCommand extends Command{
$v = (int) $input;
if($v > $max){
$sender->sendMessage(KnownTranslationFactory::commands_generic_num_tooBig($input, (string) $max)->prefix(TextFormat::RED));
$sender->sendMessage(KnownTranslationFactory::commands_generic_num_tooBig($input, (string) $max)->baseTextFormat(TextFormat::RED));
return null;
}
if($v < $min){
$sender->sendMessage(KnownTranslationFactory::commands_generic_num_tooSmall($input, (string) $min)->prefix(TextFormat::RED));
$sender->sendMessage(KnownTranslationFactory::commands_generic_num_tooSmall($input, (string) $min)->baseTextFormat(TextFormat::RED));
return null;
}

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\lang;
use pocketmine\utils\TextFormat;
use pocketmine\utils\Utils;
use Symfony\Component\Filesystem\Path;
use function array_filter;
@ -141,18 +142,26 @@ class Language{
/**
* @param (float|int|string|Translatable)[] $params
*/
public function translateString(string $str, array $params = [], ?string $onlyPrefix = null) : string{
$baseText = ($onlyPrefix === null || str_starts_with($str, $onlyPrefix)) ? $this->internalGet($str) : null;
if($baseText === null){ //key not found, embedded inside format string, or doesn't match prefix
public function translateString(string $str, array $params = [], ?string $onlyPrefix = null, string $baseFormat = "") : string{
if($onlyPrefix !== null && !str_starts_with($str, $onlyPrefix)){
//plain key for client-side translation
//% is added here if we add base format since this will turn into an embedded key
return $baseFormat !== "" ? TextFormat::addBase($baseFormat, "%" . $str) : $str;
}
$baseText = $this->internalGet($str);
if($baseText === null){ //key not found, embedded inside format string with %, or doesn't match prefix
$baseText = $this->parseTranslation($str, $onlyPrefix);
}
foreach(Utils::promoteKeys($params) as $i => $p){
$replacement = $p instanceof Translatable ? $this->translate($p) : (string) $p;
if($baseFormat !== ""){
$replacement = TextFormat::addBase($baseFormat, $replacement) . TextFormat::RESET;
}
$baseText = str_replace("{%$i}", $replacement, $baseText);
}
return $baseText;
return $baseFormat !== "" ? TextFormat::addBase($baseFormat, $baseText) : $baseText;
}
public function translate(Translatable $c) : string{
@ -161,12 +170,17 @@ class Language{
$baseText = $this->parseTranslation($c->getText());
}
$baseFormat = $c->getBaseFormat();
foreach(Utils::promoteKeys($c->getParameters()) as $i => $p){
$replacement = $p instanceof Translatable ? $this->translate($p) : $p;
if($baseFormat !== ""){
$replacement = TextFormat::addBase($baseFormat, $replacement) . TextFormat::RESET;
}
$baseText = str_replace("{%$i}", $replacement, $baseText);
}
return $baseText;
return $baseFormat !== "" ? TextFormat::addBase($baseFormat, $baseText) : $baseText;
}
protected function internalGet(string $id) : ?string{

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\lang;
use pocketmine\utils\TextFormat;
use pocketmine\utils\Utils;
final class Translatable{
@ -34,7 +35,8 @@ final class Translatable{
*/
public function __construct(
protected string $text,
array $params = []
array $params = [],
private string $baseFormat = ""
){
foreach(Utils::promoteKeys($params) as $k => $param){
if(!($param instanceof Translatable)){
@ -60,6 +62,8 @@ final class Translatable{
return $this->params[$i] ?? null;
}
public function getBaseFormat() : string{ return $this->baseFormat; }
public function format(string $before, string $after) : self{
return new self("$before%$this->text$after", $this->params);
}
@ -71,4 +75,12 @@ final class Translatable{
public function postfix(string $postfix) : self{
return new self("%$this->text" . $postfix);
}
/**
* Sets the base format to be applied to the translation result by {@link TextFormat::addBase()}.
* Any existing base format is overwritten.
*/
public function baseTextFormat(string $baseFormat) : self{
return new self($this->text, $this->params, $baseFormat);
}
}

View File

@ -767,7 +767,7 @@ class NetworkSession{
$errorId = implode("-", str_split(bin2hex(random_bytes(6)), 4));
$this->disconnect(
reason: KnownTranslationFactory::pocketmine_disconnect_error($reason, $errorId)->prefix(TextFormat::RED),
reason: KnownTranslationFactory::pocketmine_disconnect_error($reason, $errorId)->baseTextFormat(TextFormat::RED),
disconnectScreenMessage: KnownTranslationFactory::pocketmine_disconnect_error($disconnectScreenMessage ?? $reason, $errorId),
);
}
@ -1130,8 +1130,12 @@ class NetworkSession{
public function prepareClientTranslatableMessage(Translatable $message) : array{
//we can't send nested translations to the client, so make sure they are always pre-translated by the server
$language = $this->player->getLanguage();
$parameters = array_map(fn(string|Translatable $p) => $p instanceof Translatable ? $language->translate($p) : $p, $message->getParameters());
return [$language->translateString($message->getText(), $parameters, "pocketmine."), $parameters];
$baseFormat = $message->getBaseFormat();
$parameters = array_map(function(string|Translatable $p) use ($baseFormat, $language){
$string = $p instanceof Translatable ? $language->translate($p) : $p;
return $baseFormat !== "" ? TextFormat::addBase($baseFormat, $string) . TextFormat::RESET : $string;
}, $message->getParameters());
return [$language->translateString($message->getText(), $parameters, "pocketmine.", $baseFormat), $parameters];
}
public function onChatMessage(Translatable|string $message) : void{

View File

@ -411,7 +411,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
public function getLeaveMessage() : Translatable|string{
if($this->spawned){
return KnownTranslationFactory::multiplayer_player_left($this->getDisplayName())->prefix(TextFormat::YELLOW);
return KnownTranslationFactory::multiplayer_player_left($this->getDisplayName())->baseTextFormat(TextFormat::YELLOW);
}
return "";
@ -946,7 +946,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
});
$ev = new PlayerJoinEvent($this,
KnownTranslationFactory::multiplayer_player_joined($this->getDisplayName())->prefix(TextFormat::YELLOW)
KnownTranslationFactory::multiplayer_player_joined($this->getDisplayName())->baseTextFormat(TextFormat::YELLOW)
);
$ev->call();
if($ev->getJoinMessage() !== ""){

View File

@ -190,8 +190,10 @@ abstract class TextFormat{
* - Base format "§c" (red) + "Hello" (no format) = "§r§cHello"
* - Base format "§c" + "Hello §rWorld" = "§r§cHello §r§cWorld"
*
* Note: Adding base formatting to the output string a second time will result in a combination of formats from both
* calls. This is not by design, but simply a consequence of the way the function is implemented.
* Note: Adding base formatting to the output string a second time won't override conflicting formatting from the
* earlier call (e.g. adding base format BLUE to a string which already has YELLOW base formatting will
* still result in yellow text after any RESET code). However, complementary codes (e.g. italic, bold) will combine
* with the existing codes (e.g. adding ITALIC to a string with base format YELLOW will give yellow & italic text).
*/
public static function addBase(string $baseFormat, string $string) : string{
$baseFormatParts = self::tokenize($baseFormat);