Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,20 @@ Events are delivered at most once. A proxy that reconnects does not receive even

Messages are shown in English or German, following the player's client language.

### Languages and message overrides

The plugin ships English and German. A player sees the bundle for their client locale: `de_DE`, then `de`, then `en`. A key that is missing in the chosen bundle is shown in English, so a partial translation never shows a raw key.

To change or add messages without a new release, put a file named `messages_<tag>.properties` into `plugins/quelea/lang/` on the proxy (for example in the proxy template, next to `config.properties`):

- **Override single messages:** `messages_de.properties` with only the keys to change. Every other German text stays built in.
- **Add a language:** `messages_fr.properties` translates the keys it wants; the rest of the messages are English until they are translated.
- **Regional variants:** `messages_de_AT.properties` is used for Austrian clients and falls back to `de`, then `en`.

The files are read once at proxy start-up, so restart the proxy after a change. Each value is a MiniMessage template. Copy the keys from the built-in `messages_en.properties` in the plugin jar (`lang/`), and keep the placeholders (`<name>`, `<count>`, `<server>` and so on) as they are. If a template does not parse, the proxy logs a warning naming the key and language, and that message is shown in English.

The colour guide at the top of `messages_en.properties` explains which colour means what (commands, arguments, errors, hints, the leader's star).

## Configuration

### Backend (environment variables)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public void onProxyInitialize(ProxyInitializeEvent event) {
executor = Executors.newVirtualThreadPerTaskExecutor();
httpClient = HttpQueleaGateway.newClient();
QueleaGateway gateway = new HttpQueleaGateway(config.url(), httpClient, tokens);
Messages messages = Messages.load();
Messages messages = Messages.load(dataDirectory.resolve("lang"), logger);
FollowPolicy follow = FollowPolicy.of(config.lobbyPattern(), config.followMode());
VelocityLocalPlayers localPlayers = new VelocityLocalPlayers(proxy);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package net.onelitefeather.quelea.velocity.command;

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.onelitefeather.quelea.velocity.i18n.Messages;

import java.util.List;
import java.util.Locale;

/**
* The {@code /party} help: a title, a separator and one line per command. Clicking a line suggests its command in
* the chat input; hovering explains it in the player's language ({@code command.help.<name>.hover}).
*/
final class HelpText {

/** A help line: the bundle name, and the command the click suggests. */
record Entry(String name, String suggestion) {
}

static final List<Entry> ENTRIES = List.of(
new Entry("create", "/party create"),
new Entry("invite", "/party invite "),
new Entry("accept", "/party accept "),
new Entry("deny", "/party deny "),
new Entry("leave", "/party leave"),
new Entry("list", "/party list"),
new Entry("kick", "/party kick "),
new Entry("promote", "/party promote "),
new Entry("disband", "/party disband"),
new Entry("warp", "/party warp"),
new Entry("chat", "/pc "));

private HelpText() {
}

static Component of(Messages messages, Locale locale) {
Component help = messages.message(locale, "command.help.title")
.append(Component.newline())
.append(messages.line(locale, "command.help.separator"));
for (Entry entry : ENTRIES) {
help = help.append(Component.newline()).append(line(messages, locale, entry));
}
return help;
}

private static Component line(Messages messages, Locale locale, Entry entry) {
return messages.line(locale, "command.help." + entry.name())
.clickEvent(ClickEvent.suggestCommand(entry.suggestion()))
.hoverEvent(HoverEvent.showText(messages.line(locale, "command.help." + entry.name() + ".hover")));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;

/**
Expand All @@ -24,11 +24,11 @@
public final class PartyActions {

private final QueleaGateway gateway;
private final ExecutorService executor;
private final Executor executor;
private final Messages messages;
private final Logger logger;

public PartyActions(QueleaGateway gateway, ExecutorService executor, Messages messages, Logger logger) {
public PartyActions(QueleaGateway gateway, Executor executor, Messages messages, Logger logger) {
this.gateway = gateway;
this.executor = executor;
this.messages = messages;
Expand All @@ -54,7 +54,7 @@ public void invite(LocalPlayer player, String name) {
*/
public void accept(LocalPlayer player, String inviter) {
run(player, locale -> {
Optional<Invite> invite = chooseInvite(player, locale, inviter);
Optional<Invite> invite = chooseInvite(player, locale, inviter, true);
if (invite.isEmpty()) {
return;
}
Expand All @@ -68,7 +68,7 @@ public void accept(LocalPlayer player, String inviter) {
*/
public void deny(LocalPlayer player, String inviter) {
run(player, locale -> {
Optional<Invite> invite = chooseInvite(player, locale, inviter);
Optional<Invite> invite = chooseInvite(player, locale, inviter, false);
if (invite.isEmpty()) {
return;
}
Expand Down Expand Up @@ -139,27 +139,33 @@ public void chat(LocalPlayer player, String message) {
gateway.chat(party.id(), player.id().toString(), message));
}

private Optional<Invite> chooseInvite(LocalPlayer player, Locale locale, String inviter) {
List<Invite> invites = gateway.invites(player.id().toString());
if (invites.isEmpty()) {
player.send(messages.message(locale, "command.invites.none"));
return Optional.empty();
}
if (inviter == null) {
if (invites.size() == 1) {
return Optional.of(invites.get(0));
}
/**
* The invite a command refers to. Sends the answer and returns empty when there is none to pick. An accept with no
* matching invite from a player who is already in a party answers that they are already in it: that is what a
* second click on an old [Accept] button means.
*/
private Optional<Invite> chooseInvite(LocalPlayer player, Locale locale, String inviter, boolean accepting) {
String playerId = player.id().toString();
List<Invite> invites = gateway.invites(playerId);
if (inviter == null && invites.size() > 1) {
player.send(messages.message(locale, "command.invites.ambiguous",
Placeholder.unparsed("names", invites.stream().map(Invite::inviterName).collect(Collectors.joining(", ")))));
return Optional.empty();
}
Optional<Invite> match = invites.stream()
.filter(invite -> invite.inviterName().equalsIgnoreCase(inviter))
.filter(invite -> inviter == null || invite.inviterName().equalsIgnoreCase(inviter))
.findFirst();
if (match.isEmpty()) {
if (match.isPresent()) {
return match;
}
if (accepting && gateway.partyOf(playerId).isPresent()) {
player.send(messages.message(locale, "command.accept.already-member"));
} else if (invites.isEmpty()) {
player.send(messages.message(locale, "command.invites.none"));
} else {
player.send(messages.message(locale, "command.invites.unknown", Placeholder.unparsed("name", inviter)));
}
return match;
return Optional.empty();
}

private void withParty(LocalPlayer player, PartyAction action) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,18 +125,6 @@ private int playersOnly(CommandSource source, Consumer<LocalPlayer> action) {

private net.kyori.adventure.text.Component help(CommandSource source) {
Locale locale = source instanceof Player player ? player.getEffectiveLocale() : Locale.ENGLISH;
return messages.message(locale, "command.help.title")
.append(net.kyori.adventure.text.Component.newline())
.append(messages.line(locale, "command.help.create")).append(net.kyori.adventure.text.Component.newline())
.append(messages.line(locale, "command.help.invite")).append(net.kyori.adventure.text.Component.newline())
.append(messages.line(locale, "command.help.accept")).append(net.kyori.adventure.text.Component.newline())
.append(messages.line(locale, "command.help.deny")).append(net.kyori.adventure.text.Component.newline())
.append(messages.line(locale, "command.help.leave")).append(net.kyori.adventure.text.Component.newline())
.append(messages.line(locale, "command.help.list")).append(net.kyori.adventure.text.Component.newline())
.append(messages.line(locale, "command.help.kick")).append(net.kyori.adventure.text.Component.newline())
.append(messages.line(locale, "command.help.promote")).append(net.kyori.adventure.text.Component.newline())
.append(messages.line(locale, "command.help.disband")).append(net.kyori.adventure.text.Component.newline())
.append(messages.line(locale, "command.help.warp")).append(net.kyori.adventure.text.Component.newline())
.append(messages.line(locale, "command.help.chat"));
return HelpText.of(messages, locale);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import net.onelitefeather.quelea.velocity.follow.FollowPolicy;
Expand All @@ -28,6 +29,7 @@
public final class EventHandler {

private static final String EXPIRED_ALONE = "expired-alone";
private static final String DISBANDED_BY_LEADER = "leader";

private final LocalPlayers players;
private final Messages messages;
Expand All @@ -43,12 +45,14 @@ public EventHandler(LocalPlayers players, Messages messages, FollowPolicy follow

public void handle(PartyEvent event) {
switch (event.type()) {
case "party.created" -> notifyMembers(event, null, "party.created", Placeholder.unparsed("player", leaderName(event)));
case "party.created" -> notifyMembers(event, event.leaderId(), null, "party.created",
Placeholder.unparsed("player", leaderName(event)));
case "party.invited" -> invited(event);
case "party.joined" -> notifyMembers(event, event.field("playerId"), "party.joined", playerName(event));
case "party.denied" -> notifyMembers(event, event.field("playerId"), "party.denied", playerName(event));
case "party.left" -> notifyMembers(event, event.field("playerId"), "party.left", playerName(event));
case "party.kicked" -> notifyMembers(event, event.field("playerId"), "party.kicked", playerName(event));
case "party.joined" -> notifyOthers(event, "party.joined", playerName(event));
case "party.denied" -> notifyOthers(event, "party.denied", playerName(event));
case "party.left" -> notifyOthers(event, "party.left", playerName(event));
case "party.kicked" -> notifyMembers(event, event.leaderId(), event.field("playerId"), "party.kicked",
playerName(event));
case "party.leader-changed" -> leaderChanged(event);
case "party.disbanded" -> disbanded(event);
case "party.expiring" -> expiring(event);
Expand All @@ -67,10 +71,15 @@ private void invited(PartyEvent event) {
return;
}
players.find(target).ifPresent(player -> {
TagResolver inviter = Placeholder.unparsed("inviter", inviterName);
Component accept = messages.line(player.locale(), "party.invite.accept")
.clickEvent(ClickEvent.runCommand("/party accept " + inviterName));
.clickEvent(ClickEvent.runCommand("/party accept " + inviterName))
.hoverEvent(HoverEvent.showText(
messages.line(player.locale(), "party.invite.accept.hover", inviter)));
Component deny = messages.line(player.locale(), "party.invite.deny")
.clickEvent(ClickEvent.runCommand("/party deny " + inviterName));
.clickEvent(ClickEvent.runCommand("/party deny " + inviterName))
.hoverEvent(HoverEvent.showText(
messages.line(player.locale(), "party.invite.deny.hover", inviter)));
player.send(messages.message(player.locale(), "party.invited",
Placeholder.unparsed("inviter", inviterName))
.append(Component.space())
Expand All @@ -82,26 +91,37 @@ private void invited(PartyEvent event) {

/**
* Sends {@code key} to the members of the event, and {@code key.self} to the affected player (who may not be a
* member any more). The affected player never gets both.
* member any more). The actor of the command never gets either: their command answer already says it.
*
* @param actorId the player who ran the command, or {@code null} when the event does not name one
* @param affectedId the player the change is about, or {@code null}
*/
private void notifyMembers(PartyEvent event, String affectedId, String key, TagResolver... resolvers) {
if (affectedId != null) {
private void notifyMembers(PartyEvent event, String actorId, String affectedId, String key,
TagResolver... resolvers) {
if (affectedId != null && !affectedId.equals(actorId)) {
sendTo(affectedId, key + ".self", resolvers);
}
for (Member member : event.members()) {
if (!member.id().equals(affectedId)) {
if (!member.id().equals(affectedId) && !member.id().equals(actorId)) {
sendTo(member.id(), key, resolvers);
}
}
}

/** For events where the actor is the affected player: everyone else in the party hears about it. */
private void notifyOthers(PartyEvent event, String key, TagResolver... resolvers) {
String actorId = event.field("playerId");
notifyMembers(event, actorId, actorId, key, resolvers);
}

private void leaderChanged(PartyEvent event) {
String leaderId = event.leaderId();
if (leaderId != null) {
String actorId = event.field("previousLeaderId");
if (leaderId != null && !leaderId.equals(actorId)) {
sendTo(leaderId, "party.leader-changed.self");
}
for (Member member : event.members()) {
if (!member.id().equals(leaderId)) {
if (!member.id().equals(leaderId) && !member.id().equals(actorId)) {
sendTo(member.id(), "party.leader-changed", Placeholder.unparsed("player", leaderName(event)));
}
}
Expand All @@ -112,9 +132,14 @@ private void disbanded(PartyEvent event) {
return;
}
// Unknown reasons keep the generic message, so the service can add reasons without a plugin release.
String key = EXPIRED_ALONE.equals(event.field("reason")) ? "party.disbanded.expired-alone" : "party.disbanded";
String reason = event.field("reason");
String key = EXPIRED_ALONE.equals(reason) ? "party.disbanded.expired-alone" : "party.disbanded";
// A leader who disbanded the party already read the command's answer.
String actor = DISBANDED_BY_LEADER.equals(reason) ? event.leaderId() : null;
for (PlayerRef former : QueleaJson.players(event.payload().getAsJsonArray("formerMembers"))) {
sendTo(former.id(), key);
if (!former.id().equals(actor)) {
sendTo(former.id(), key);
}
}
}

Expand Down
Loading
Loading