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){ 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 !== ""){ }elseif($this->permissionMessage !== ""){
$target->sendMessage(str_replace("<permission>", $permission ?? implode(";", $this->permission), $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{ public static function broadcastCommandMessage(CommandSender $source, Translatable|string $message, bool $sendToSource = true) : void{
$users = $source->getServer()->getBroadcastChannelSubscribers(Server::BROADCAST_CHANNEL_ADMINISTRATIVE); $users = $source->getServer()->getBroadcastChannelSubscribers(Server::BROADCAST_CHANNEL_ADMINISTRATIVE);
$result = KnownTranslationFactory::chat_type_admin($source->getName(), $message); $result = KnownTranslationFactory::chat_type_admin($source->getName(), $message);
$colored = $result->prefix(TextFormat::GRAY . TextFormat::ITALIC); $colored = $result->baseTextFormat(TextFormat::GRAY . TextFormat::ITALIC);
if($sendToSource){ if($sendToSource){
$source->sendMessage($message); $source->sendMessage($message);

View File

@ -107,7 +107,7 @@ class FormattedCommandAlias extends Command{
$timings->stopTiming(); $timings->stopTiming();
} }
}else{ }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() //to match the behaviour of SimpleCommandMap::dispatch()
//this shouldn't normally happen, but might happen if the command was unregistered or modified after //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; 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; return false;
} }

View File

@ -69,7 +69,7 @@ class ClearCommand extends VanillaCommand{
} }
}catch(LegacyStringToItemParserException $e){ }catch(LegacyStringToItemParserException $e){
//vanilla checks this at argument parsing layer, can't come up with a better alternative //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; return true;
} }
} }
@ -90,7 +90,7 @@ class ClearCommand extends VanillaCommand{
if($count > 0){ if($count > 0){
$sender->sendMessage(KnownTranslationFactory::commands_clear_testing($target->getName(), (string) $count)); $sender->sendMessage(KnownTranslationFactory::commands_clear_testing($target->getName(), (string) $count));
}else{ }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; return true;
@ -132,7 +132,7 @@ class ClearCommand extends VanillaCommand{
if($clearedCount > 0){ if($clearedCount > 0){
Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_clear_success($target->getName(), (string) $clearedCount)); Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_clear_success($target->getName(), (string) $clearedCount));
}else{ }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; return true;

View File

@ -68,7 +68,7 @@ class EffectCommand extends VanillaCommand{
$effect = StringToEffectParser::getInstance()->parse($args[1]); $effect = StringToEffectParser::getInstance()->parse($args[1]);
if($effect === null){ 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; return true;
} }

View File

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

View File

@ -105,22 +105,22 @@ class HelpCommand extends VanillaCommand{
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_header($cmd->getLabel()) $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_header($cmd->getLabel())
->format(TextFormat::YELLOW . "--------- " . TextFormat::RESET, TextFormat::YELLOW . " ---------")); ->format(TextFormat::YELLOW . "--------- " . TextFormat::RESET, TextFormat::YELLOW . " ---------"));
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_description(TextFormat::RESET . $descriptionString) $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_description(TextFormat::RESET . $descriptionString)
->prefix(TextFormat::GOLD)); ->baseTextFormat(TextFormat::GOLD));
$usage = $cmd->getUsage(); $usage = $cmd->getUsage();
$usageString = $usage instanceof Translatable ? $lang->translate($usage) : $usage; $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))) $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(); $aliases = $cmd->getAliases();
sort($aliases, SORT_NATURAL); sort($aliases, SORT_NATURAL);
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_aliases(TextFormat::RESET . implode(", ", $aliases)) $sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_aliases(TextFormat::RESET . implode(", ", $aliases))
->prefix(TextFormat::GOLD)); ->baseTextFormat(TextFormat::GOLD));
return true; 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; return true;
} }

View File

@ -114,7 +114,7 @@ class ParticleCommand extends VanillaCommand{
$particle = $this->getParticle($name, $data); $particle = $this->getParticle($name, $data);
if($particle === null){ 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; return true;
} }

View File

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

View File

@ -60,9 +60,9 @@ class TellCommand extends VanillaCommand{
if($player instanceof Player){ if($player instanceof Player){
$message = implode(" ", $args); $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(); $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); Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_message_display_outgoing($player->getDisplayName(), $message), false);
}else{ }else{
$sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()); $sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound());

View File

@ -99,11 +99,11 @@ abstract class VanillaCommand extends Command{
$v = (int) $input; $v = (int) $input;
if($v > $max){ 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; return null;
} }
if($v < $min){ 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; return null;
} }

View File

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

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\lang; namespace pocketmine\lang;
use pocketmine\utils\TextFormat;
use pocketmine\utils\Utils; use pocketmine\utils\Utils;
final class Translatable{ final class Translatable{
@ -34,7 +35,8 @@ final class Translatable{
*/ */
public function __construct( public function __construct(
protected string $text, protected string $text,
array $params = [] array $params = [],
private string $baseFormat = ""
){ ){
foreach(Utils::promoteKeys($params) as $k => $param){ foreach(Utils::promoteKeys($params) as $k => $param){
if(!($param instanceof Translatable)){ if(!($param instanceof Translatable)){
@ -60,6 +62,8 @@ final class Translatable{
return $this->params[$i] ?? null; return $this->params[$i] ?? null;
} }
public function getBaseFormat() : string{ return $this->baseFormat; }
public function format(string $before, string $after) : self{ public function format(string $before, string $after) : self{
return new self("$before%$this->text$after", $this->params); return new self("$before%$this->text$after", $this->params);
} }
@ -71,4 +75,12 @@ final class Translatable{
public function postfix(string $postfix) : self{ public function postfix(string $postfix) : self{
return new self("%$this->text" . $postfix); 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)); $errorId = implode("-", str_split(bin2hex(random_bytes(6)), 4));
$this->disconnect( $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), disconnectScreenMessage: KnownTranslationFactory::pocketmine_disconnect_error($disconnectScreenMessage ?? $reason, $errorId),
); );
} }
@ -1130,8 +1130,12 @@ class NetworkSession{
public function prepareClientTranslatableMessage(Translatable $message) : array{ 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 //we can't send nested translations to the client, so make sure they are always pre-translated by the server
$language = $this->player->getLanguage(); $language = $this->player->getLanguage();
$parameters = array_map(fn(string|Translatable $p) => $p instanceof Translatable ? $language->translate($p) : $p, $message->getParameters()); $baseFormat = $message->getBaseFormat();
return [$language->translateString($message->getText(), $parameters, "pocketmine."), $parameters]; $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{ 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{ public function getLeaveMessage() : Translatable|string{
if($this->spawned){ if($this->spawned){
return KnownTranslationFactory::multiplayer_player_left($this->getDisplayName())->prefix(TextFormat::YELLOW); return KnownTranslationFactory::multiplayer_player_left($this->getDisplayName())->baseTextFormat(TextFormat::YELLOW);
} }
return ""; return "";
@ -946,7 +946,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}); });
$ev = new PlayerJoinEvent($this, $ev = new PlayerJoinEvent($this,
KnownTranslationFactory::multiplayer_player_joined($this->getDisplayName())->prefix(TextFormat::YELLOW) KnownTranslationFactory::multiplayer_player_joined($this->getDisplayName())->baseTextFormat(TextFormat::YELLOW)
); );
$ev->call(); $ev->call();
if($ev->getJoinMessage() !== ""){ if($ev->getJoinMessage() !== ""){

View File

@ -190,8 +190,10 @@ abstract class TextFormat{
* - Base format "§c" (red) + "Hello" (no format) = "§r§cHello" * - Base format "§c" (red) + "Hello" (no format) = "§r§cHello"
* - Base format "§c" + "Hello §rWorld" = "§r§cHello §r§cWorld" * - 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 * Note: Adding base formatting to the output string a second time won't override conflicting formatting from the
* calls. This is not by design, but simply a consequence of the way the function is implemented. * 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{ public static function addBase(string $baseFormat, string $string) : string{
$baseFormatParts = self::tokenize($baseFormat); $baseFormatParts = self::tokenize($baseFormat);