Skip to content

Commit acd78e5

Browse files
committed
feat(translate): add automatic message translation to player language
- Created TranslatedMessage model to store all message translations - Extended HistoryMessage to support original/translation toggle - Added /toggleoriginal command to switch between original and translation - Implemented automatic translation to all unique server locales (API request optimization) - Updated localizations: button now toggles original/translation instead of creating new message - Added PulseAutoTranslateListener to handle auto-translation in MessagePrepareEvent - Modified DeleteModule to work with translations and toggling Now each message is automatically translated to the receiver's language. Button on message allows switching between original and translation.
1 parent 8da7eb3 commit acd78e5

10 files changed

Lines changed: 367 additions & 29 deletions

File tree

core/src/main/java/net/flectone/pulse/module/command/CommandModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import net.flectone.pulse.module.command.symbol.SymbolModule;
4747
import net.flectone.pulse.module.command.tell.TellModule;
4848
import net.flectone.pulse.module.command.tictactoe.TictactoeModule;
49+
import net.flectone.pulse.module.command.toggleoriginal.ToggleOriginalModule;
4950
import net.flectone.pulse.module.command.toponline.ToponlineModule;
5051
import net.flectone.pulse.module.command.translateto.TranslatetoModule;
5152
import net.flectone.pulse.module.command.try_.TryModule;
@@ -105,6 +106,7 @@ public class CommandModule implements ModuleSimple {
105106
SymbolModule.class,
106107
TellModule.class,
107108
TictactoeModule.class,
109+
ToggleOriginalModule.class,
108110
ToponlineModule.class,
109111
TranslatetoModule.class,
110112
TryModule.class,
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package net.flectone.pulse.module.command.toggleoriginal;
2+
3+
import com.google.inject.Inject;
4+
import com.google.inject.Singleton;
5+
import lombok.RequiredArgsConstructor;
6+
import net.flectone.pulse.config.Command;
7+
import net.flectone.pulse.config.Localization;
8+
import net.flectone.pulse.config.Permission;
9+
import net.flectone.pulse.model.entity.FEntity;
10+
import net.flectone.pulse.model.entity.FPlayer;
11+
import net.flectone.pulse.module.ModuleCommand;
12+
import net.flectone.pulse.module.message.format.moderation.delete.DeleteModule;
13+
import net.flectone.pulse.platform.controller.ModuleCommandController;
14+
import net.flectone.pulse.platform.controller.ModuleController;
15+
import net.flectone.pulse.platform.provider.CommandParserProvider;
16+
import net.flectone.pulse.util.constant.ModuleName;
17+
import net.flectone.pulse.util.file.FileFacade;
18+
import org.incendo.cloud.context.CommandContext;
19+
20+
import java.util.UUID;
21+
22+
@Singleton
23+
@RequiredArgsConstructor(onConstructor = @__(@Inject))
24+
public class ToggleOriginalModule implements ModuleCommand<Localization.Command.Translateto> {
25+
26+
private final FileFacade fileFacade;
27+
private final CommandParserProvider commandParserProvider;
28+
private final DeleteModule deleteModule;
29+
private final ModuleController moduleController;
30+
private final ModuleCommandController commandModuleController;
31+
32+
@Override
33+
public void onEnable() {
34+
String promptMessage = commandModuleController.addPrompt(this, 0, Localization.Command.Prompt::message);
35+
commandModuleController.registerCommand(this, manager -> manager
36+
.literal("toggleoriginal")
37+
.required(promptMessage, commandParserProvider.singleMessageParser())
38+
.permission(permission().name())
39+
);
40+
}
41+
42+
@Override
43+
public void onDisable() {
44+
commandModuleController.clearPrompts(this);
45+
}
46+
47+
@Override
48+
public void execute(FPlayer fPlayer, CommandContext<FPlayer> commandContext) {
49+
if (moduleController.isDisabledFor(this, fPlayer, true)) return;
50+
51+
String messageUuidString = commandModuleController.getArgument(this, commandContext, 0);
52+
53+
try {
54+
UUID messageUuid = UUID.fromString(messageUuidString);
55+
deleteModule.toggleOriginal(fPlayer, messageUuid);
56+
} catch (IllegalArgumentException _) {
57+
// Invalid UUID, ignore
58+
}
59+
}
60+
61+
@Override
62+
public ModuleName name() {
63+
return ModuleName.COMMAND_TRANSLATETO;
64+
}
65+
66+
@Override
67+
public Command.Translateto config() {
68+
return fileFacade.command().translateto();
69+
}
70+
71+
@Override
72+
public Permission.Command.Translateto permission() {
73+
return fileFacade.permission().command().translateto();
74+
}
75+
76+
@Override
77+
public Localization.Command.Translateto localization(FEntity sender) {
78+
return fileFacade.localization(sender).command().translateto();
79+
}
80+
}

core/src/main/java/net/flectone/pulse/module/message/format/moderation/delete/DeleteModule.java

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,17 @@ public MessageContext addTag(MessageContext messageContext) {
110110
}
111111

112112
public void save(FPlayer receiver, UUID messageUUID, Component component, boolean needToCache) {
113+
save(receiver, messageUUID, component, null, needToCache);
114+
}
115+
116+
public void save(FPlayer receiver, UUID messageUUID, Component component, net.flectone.pulse.module.message.format.translate.model.TranslatedMessage translatedMessage, boolean needToCache) {
113117
// skip console
114118
if (receiver.isUnknown()) return;
115119
// skip offline history
116120
if (!receiver.isOnline()) return;
117121

118122
UUID playerUUID = receiver.uuid();
119-
HistoryMessage historyMessage = new HistoryMessage(messageUUID, component);
123+
HistoryMessage historyMessage = new HistoryMessage(messageUUID, component, translatedMessage);
120124

121125
List<HistoryMessage> history = playersHistory.computeIfAbsent(playerUUID, _ -> new ObjectArrayList<>());
122126

@@ -184,6 +188,7 @@ public void sendUpdate(UUID receiver) {
184188
if (history == null) return;
185189

186190
FPlayer fPlayer = fPlayerService.getFPlayer(receiver);
191+
String playerLocale = fPlayer.getSetting(net.flectone.pulse.util.constant.SettingText.LOCALE);
187192

188193
// empty messages
189194
for (int i = 0; i < config().historyLength(); i++) {
@@ -193,7 +198,34 @@ public void sendUpdate(UUID receiver) {
193198
}
194199

195200
history.forEach(historyMessage ->
196-
messageSender.sendMessage(fPlayer, historyMessage.component(), true)
201+
messageSender.sendMessage(fPlayer, historyMessage.getDisplayComponent(playerLocale), true)
197202
);
198203
}
204+
205+
public boolean toggleOriginal(FPlayer fPlayer, UUID messageUUID) {
206+
if (moduleController.isDisabledFor(this, fPlayer)) return false;
207+
if (messageUUID == null) return false;
208+
209+
UUID playerUUID = fPlayer.uuid();
210+
List<HistoryMessage> history = playersHistory.get(playerUUID);
211+
if (history == null) return false;
212+
213+
boolean updated = false;
214+
for (int i = 0; i < history.size(); i++) {
215+
HistoryMessage historyMessage = history.get(i);
216+
if (messageUUID.equals(historyMessage.uuid()) && historyMessage.hasTranslations()) {
217+
// Toggle showOriginal flag
218+
HistoryMessage updatedMessage = historyMessage.withShowOriginal(!historyMessage.showOriginal());
219+
history.set(i, updatedMessage);
220+
updated = true;
221+
break;
222+
}
223+
}
224+
225+
if (updated) {
226+
sendUpdate(playerUUID);
227+
}
228+
229+
return updated;
230+
}
199231
}
Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,47 @@
11
package net.flectone.pulse.module.message.format.moderation.delete.model;
22

3+
import lombok.With;
4+
import net.flectone.pulse.module.message.format.translate.model.TranslatedMessage;
35
import net.kyori.adventure.text.Component;
6+
import org.jspecify.annotations.Nullable;
47

58
import java.util.UUID;
69

7-
public record HistoryMessage(UUID uuid, Component component) {
10+
@With
11+
public record HistoryMessage(
12+
UUID uuid,
13+
Component component,
14+
@Nullable TranslatedMessage translatedMessage,
15+
boolean showOriginal
16+
) {
17+
18+
public HistoryMessage(UUID uuid, Component component) {
19+
this(uuid, component, null, false);
20+
}
21+
22+
public HistoryMessage(UUID uuid, Component component, TranslatedMessage translatedMessage) {
23+
this(uuid, component, translatedMessage, false);
24+
}
25+
26+
/**
27+
* Get the component to display based on showOriginal flag and player locale.
28+
*/
29+
public Component getDisplayComponent(String playerLocale) {
30+
if (translatedMessage == null) {
31+
return component;
32+
}
33+
34+
if (showOriginal) {
35+
return translatedMessage.getOriginal();
36+
}
37+
38+
return translatedMessage.getTranslation(playerLocale);
39+
}
40+
41+
/**
42+
* Check if this message has translations.
43+
*/
44+
public boolean hasTranslations() {
45+
return translatedMessage != null;
46+
}
847
}

core/src/main/java/net/flectone/pulse/module/message/format/translate/TranslateModule.java

Lines changed: 70 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,38 +13,49 @@
1313
import net.flectone.pulse.model.entity.FPlayer;
1414
import net.flectone.pulse.model.event.message.context.MessageContext;
1515
import net.flectone.pulse.module.ModuleLocalization;
16+
import net.flectone.pulse.module.command.translateto.TranslatetoModule;
1617
import net.flectone.pulse.module.message.format.translate.listener.PulseTranslateListener;
18+
import net.flectone.pulse.module.message.format.translate.model.TranslatedMessage;
1719
import net.flectone.pulse.platform.controller.ModuleController;
1820
import net.flectone.pulse.platform.registry.ListenerRegistry;
21+
import net.flectone.pulse.service.FPlayerService;
1922
import net.flectone.pulse.util.constant.MessageFlag;
2023
import net.flectone.pulse.util.constant.ModuleName;
2124
import net.flectone.pulse.util.constant.SettingText;
2225
import net.flectone.pulse.util.file.FileFacade;
26+
import net.kyori.adventure.text.Component;
2327
import net.kyori.adventure.text.minimessage.tag.Tag;
28+
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
2429
import org.apache.commons.lang3.Strings;
2530
import org.jspecify.annotations.Nullable;
2631

27-
import java.util.Map;
28-
import java.util.UUID;
32+
import java.util.*;
33+
import java.util.concurrent.ConcurrentHashMap;
34+
import java.util.stream.Collectors;
2935

3036
@Singleton
3137
@RequiredArgsConstructor(onConstructor = @__(@Inject))
3238
public class TranslateModule implements ModuleLocalization<Localization.Message.Format.Translate> {
3339

3440
private final @Named("translateMessage") Cache<String, UUID> messageCache;
41+
private final Map<UUID, TranslatedMessage> translatedMessages = new ConcurrentHashMap<>();
3542
private final FileFacade fileFacade;
3643
private final ListenerRegistry listenerRegistry;
3744
private final MessagePipeline messagePipeline;
3845
private final ModuleController moduleController;
46+
private final FPlayerService fPlayerService;
47+
private final TranslatetoModule translatetoModule;
3948

4049
@Override
4150
public void onEnable() {
4251
listenerRegistry.register(PulseTranslateListener.class);
52+
listenerRegistry.register(net.flectone.pulse.module.message.format.translate.listener.PulseAutoTranslateListener.class);
4353
}
4454

4555
@Override
4656
public void onDisable() {
4757
messageCache.invalidateAll();
58+
translatedMessages.clear();
4859
}
4960

5061
@Override
@@ -84,30 +95,10 @@ public MessageContext addTag(MessageContext messageContext) {
8495
FPlayer receiver = messageContext.receiver();
8596

8697
return messageContext.addTagResolver(MessagePipeline.ReplacementTag.TRANSLATION, (argumentQueue, _) -> {
87-
String firstLang = "auto";
88-
String secondLang = receiver.getSetting(SettingText.LOCALE);
89-
90-
if (argumentQueue.hasNext()) {
91-
Tag.Argument first = argumentQueue.pop();
92-
93-
if (argumentQueue.hasNext()) {
94-
Tag.Argument second = argumentQueue.pop();
95-
96-
if (argumentQueue.hasNext()) {
97-
// translateto language language message
98-
firstLang = first.value();
99-
secondLang = second.value();
100-
} else {
101-
// translateto auto language message
102-
secondLang = first.value();
103-
}
104-
}
105-
}
98+
UUID messageUUID = messageContext.messageUUID();
10699

107100
String action = localization(receiver).action();
108-
action = Strings.CS.replaceOnce(action, "<language>", firstLang);
109-
action = Strings.CS.replaceOnce(action, "<language>", secondLang == null ? "ru_ru" : secondLang);
110-
action = Strings.CS.replace(action, "<message>", saveMessage(messageContext.userMessage()).toString());
101+
action = Strings.CS.replace(action, "<message>", messageUUID.toString());
111102

112103
MessageContext tagContext = messagePipeline.createContext(sender, receiver, action)
113104
.withFlags(messageContext.flags())
@@ -120,6 +111,61 @@ public MessageContext addTag(MessageContext messageContext) {
120111
});
121112
}
122113

114+
/**
115+
* Translate message to all unique locales on server.
116+
* Returns TranslatedMessage with all translations.
117+
*/
118+
public TranslatedMessage translateToAllLocales(String originalText, String sourceLang) {
119+
if (moduleController.isDisabledFor(this, FPlayer.UNKNOWN)) return null;
120+
121+
// Get unique locales from online players
122+
Set<String> uniqueLocales = fPlayerService.getOnlineFPlayers().stream()
123+
.map(player -> player.getSetting(SettingText.LOCALE))
124+
.filter(Objects::nonNull)
125+
.collect(Collectors.toSet());
126+
127+
// Always include source language
128+
uniqueLocales.add(sourceLang);
129+
130+
UUID messageUUID = UUID.randomUUID();
131+
TranslatedMessage.TranslatedMessageBuilder builder = TranslatedMessage.builder()
132+
.uuid(messageUUID)
133+
.originalText(originalText)
134+
.originalLang(sourceLang);
135+
136+
Map<String, Component> translations = new ConcurrentHashMap<>();
137+
138+
// Translate to each unique locale
139+
for (String targetLang : uniqueLocales) {
140+
Component translatedComponent;
141+
142+
if (targetLang.equals(sourceLang)) {
143+
// No translation needed for source language
144+
translatedComponent = Component.text(originalText);
145+
} else {
146+
// Translate using TranslatetoModule
147+
String translated = translatetoModule.translate(FPlayer.UNKNOWN, sourceLang, targetLang, originalText);
148+
translatedComponent = translated.isEmpty()
149+
? Component.text(originalText)
150+
: Component.text(translated);
151+
}
152+
153+
translations.put(targetLang, translatedComponent);
154+
}
155+
156+
TranslatedMessage translatedMessage = builder.translations(translations).build();
157+
translatedMessages.put(messageUUID, translatedMessage);
158+
159+
return translatedMessage;
160+
}
161+
162+
/**
163+
* Get TranslatedMessage by UUID.
164+
*/
165+
public @Nullable TranslatedMessage getTranslatedMessage(UUID uuid) {
166+
return translatedMessages.get(uuid);
167+
}
168+
123169
public String getMessage(String stringUUID) {
124170
try {
125171
UUID uuid = UUID.fromString(stringUUID);

0 commit comments

Comments
 (0)