Skip to content

Commit

Permalink
feat(api): add 'i18n' config for translations (#48)
Browse files Browse the repository at this point in the history
fix: modded translation keys should be resolved (fixes #45)
  this change will load all modded translation files at startup

refactor(chat): move world names option to the `api` module
  • Loading branch information
axieum committed Aug 5, 2022
1 parent c343319 commit 64b95c0
Show file tree
Hide file tree
Showing 24 changed files with 293 additions and 184 deletions.
6 changes: 3 additions & 3 deletions gradle.properties
Expand Up @@ -8,9 +8,9 @@ mod_version = 1.0.0-beta.3

# Fabric
minecraft_version = 1.19
loader_version = 0.14.6
yarn_mappings = 1.19+build.1
fabric_version = 0.55.2+1.19
loader_version = 0.14.8
yarn_mappings = 1.19+build.4
fabric_version = 0.58.0+1.19

# Dependencies
checkstyle_version = 10.3
Expand Down
Expand Up @@ -2,6 +2,7 @@

import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.HashMap;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.MatchResult;
Expand All @@ -10,15 +11,24 @@
import com.vdurmont.emoji.EmojiParser;
import net.dv8tion.jda.api.entities.IMentionable;

import net.minecraft.advancement.AdvancementFrame;
import net.minecraft.util.Formatting;
import net.minecraft.util.Identifier;
import net.minecraft.world.World;

import me.axieum.mcmod.minecord.api.Minecord;
import static me.axieum.mcmod.minecord.impl.MinecordImpl.getConfig;

/**
* Utility methods for building and manipulating strings.
*/
public final class StringUtils
{
// Mapping of Minecraft world identifiers to their human-readable names
public static final HashMap<Identifier, String> WORLD_NAMES = new HashMap<>(3);
// String templates for translating between Minecraft and Discord formatted strings
public static StringTemplate discordMinecraftST, minecraftDiscordST;

private StringUtils() {}

/**
Expand All @@ -38,9 +48,6 @@ public static String bytesToHuman(long bytes)
return String.format("%.1f %cB", bytes / 1000.0, ci.current());
}

// String templates for translating between Minecraft and Discord formatted strings
public static StringTemplate discordMinecraftST, minecraftDiscordST;

static {
final Pattern bold = Pattern.compile("\\*\\*(.+?)\\*\\*");
final Pattern underline = Pattern.compile("__(.+?)__");
Expand Down Expand Up @@ -83,21 +90,21 @@ public static String bytesToHuman(long bytes)
final Pattern channel = Pattern.compile("#([^\\s]+)");
final Function<MatchResult, String> resolveMention = m ->
Minecord.getInstance().getJDA()
.flatMap(jda -> Optional.ofNullable(jda.getUserByTag(m.group(1), m.group(2))))
.map(IMentionable::getAsMention)
.orElse(m.group(0));
.flatMap(jda -> Optional.ofNullable(jda.getUserByTag(m.group(1), m.group(2))))
.map(IMentionable::getAsMention)
.orElse(m.group(0));
final Function<MatchResult, String> resolveMention2 = m ->
Minecord.getInstance().getJDA()
.flatMap(jda -> jda.getGuilds().stream()
.flatMap(g -> g.getMembersByEffectiveName(m.group(1), true).stream())
.findFirst())
.map(IMentionable::getAsMention)
.orElse(m.group(0));
.flatMap(jda -> jda.getGuilds().stream()
.flatMap(g -> g.getMembersByEffectiveName(m.group(1), true).stream())
.findFirst())
.map(IMentionable::getAsMention)
.orElse(m.group(0));
final Function<MatchResult, String> resolveChannel = m ->
Minecord.getInstance().getJDA()
.flatMap(jda -> jda.getTextChannelsByName(m.group(1), true).stream().findFirst())
.map(IMentionable::getAsMention)
.orElse(m.group(0));
.flatMap(jda -> jda.getTextChannelsByName(m.group(1), true).stream().findFirst())
.map(IMentionable::getAsMention)
.orElse(m.group(0));
// Construct the string template
minecraftDiscordST = new StringTemplate()
// Collapse line breaks
Expand All @@ -121,7 +128,7 @@ public static String bytesToHuman(long bytes)
// Suppress @everyone and @here mentions
.transform(s -> s.replace("@everyone", "@_everyone_"))
.transform(s -> s.replace("@here", "@_here_"))
// Strip any left over formatting
// Strip any leftover formatting
.transform(Formatting::strip);
}

Expand All @@ -132,7 +139,7 @@ public static String bytesToHuman(long bytes)
* @param contents Discord flavoured markdown string
* @return Minecraft-formatted string
*/
public static String discordToMinecraft(String contents)
public static String discordToMinecraft(final String contents)
{
// Apply the appropriate string template against the given contents and return
return discordMinecraftST.format(contents);
Expand All @@ -149,4 +156,59 @@ public static String minecraftToDiscord(final String contents)
// Apply the appropriate string template against the given contents and return
return minecraftDiscordST.format(contents);
}

/**
* Attempts to retrieve the world name from the config files first,
* otherwise derives it from the registry key.
*
* @param world Minecraft world
* @return name of the given world
* @see #deriveWorldName(Identifier)
*/
public static String getWorldName(final World world)
{
final Identifier identifier = world.getRegistryKey().getValue();
return getConfig().i18n.worlds.getOrDefault(identifier.toString(), deriveWorldName(identifier));
}

/**
* Attempts to compute and cache the world name from its registry key.
* NB: At present, the world name is not stored in any resources, apart
* from in the registry key, e.g. 'the_nether'.
*
* @param identifier Minecraft world identifier
* @return derived name of the given world identifier
*/
public static String deriveWorldName(final Identifier identifier)
{
return WORLD_NAMES.computeIfAbsent(identifier, id -> {
// Space delimited identifier path, with leading 'the' keywords removed
final String path = id.getPath().replace('_', ' ').replaceFirst("(?i)the\\s", "");
// Capitalise the first character in each word
char[] chars = path.toCharArray();
boolean capitalizeNext = true;
for (int i = 0; i < chars.length; i++) {
if (chars[i] == ' ') {
capitalizeNext = true;
} else if (capitalizeNext) {
chars[i] = Character.toTitleCase(chars[i]);
capitalizeNext = false;
}
}
// Return the computed world name
return new String(chars);
});
}

/**
* Attempts to retrieve the advancement type name from the config files
* first, otherwise uses it symbol name.
*
* @param type Minecraft advancement frame/type
* @return name of the advancement type
*/
public static String getAdvancementTypeName(final AdvancementFrame type)
{
return getConfig().i18n.advancementTypes.getOrDefault(type.getId(), type.getId());
}
}
Expand Up @@ -45,14 +45,14 @@ public void onPreLaunch()

try {
// Prepare the JDA client
final JDABuilder builder = JDABuilder.createDefault(getConfig().token)
final JDABuilder builder = JDABuilder.createDefault(getConfig().bot.token)
// set initial bot status
.setStatus(getConfig().status.starting)
.setStatus(getConfig().bot.status.starting)
// add event listeners
.addEventListeners(new DiscordLifecycleListener());

// Conditionally enable member caching
if (getConfig().cacheMembers) {
if (getConfig().bot.cacheMembers) {
builder.enableIntents(GatewayIntent.GUILD_MEMBERS) // enable required intents
.setMemberCachePolicy(MemberCachePolicy.ALL) // cache all members
.setChunkingFilter(ChunkingFilter.ALL); // eager-load all members
Expand Down
Expand Up @@ -20,29 +20,29 @@ public class ServerLifecycleCallback implements ServerStarting, ServerStarted, S
public void onServerStarting(MinecraftServer server)
{
// Update the Discord bot status
Minecord.getInstance().getJDA().ifPresent(jda -> jda.getPresence().setStatus(getConfig().status.starting));
Minecord.getInstance().getJDA().ifPresent(jda -> jda.getPresence().setStatus(getConfig().bot.status.starting));
}

@Override
public void onServerStarted(MinecraftServer server)
{
// Update the Discord bot status
Minecord.getInstance().getJDA().ifPresent(jda -> jda.getPresence().setStatus(getConfig().status.started));
Minecord.getInstance().getJDA().ifPresent(jda -> jda.getPresence().setStatus(getConfig().bot.status.started));
}

@Override
public void onServerStopping(MinecraftServer server)
{
// Update the Discord bot status
Minecord.getInstance().getJDA().ifPresent(jda -> jda.getPresence().setStatus(getConfig().status.stopping));
Minecord.getInstance().getJDA().ifPresent(jda -> jda.getPresence().setStatus(getConfig().bot.status.stopping));
}

@Override
public void onServerShutdown(MinecraftServer server, @Nullable CrashReport crashReport)
{
Minecord.getInstance().getJDA().ifPresent(jda -> {
// Update the Discord bot status
jda.getPresence().setStatus(getConfig().status.stopped);
jda.getPresence().setStatus(getConfig().bot.status.stopped);
// Shutdown the JDA client
LOGGER.info("Minecord is wrapping up...");
jda.shutdown();
Expand Down
@@ -0,0 +1,43 @@
package me.axieum.mcmod.minecord.impl.config;

import me.shedaniel.autoconfig.ConfigData;
import me.shedaniel.autoconfig.annotation.Config;
import me.shedaniel.autoconfig.annotation.ConfigEntry.Category;
import me.shedaniel.autoconfig.annotation.ConfigEntry.Gui.RequiresRestart;
import me.shedaniel.cloth.clothconfig.shadowed.blue.endless.jankson.Comment;
import net.dv8tion.jda.api.OnlineStatus;

@Config(name = "bot")
public class BotConfig implements ConfigData
{
@Comment("Token used to authenticate against your Discord bot")
@RequiresRestart
public String token = "";

@Category("Bot Status")
@Comment("Bot statuses relayed during the lifecycle of the server")
public StatusSchema status = new StatusSchema();

/**
* Bot status configuration schema.
*/
public static class StatusSchema
{
@Comment("Status while the server is starting")
public OnlineStatus starting = OnlineStatus.IDLE;

@Comment("Status after the server has started")
public OnlineStatus started = OnlineStatus.ONLINE;

@Comment("Status while the server is stopping")
public OnlineStatus stopping = OnlineStatus.DO_NOT_DISTURB;

@Comment("Status after the server has stopped")
public OnlineStatus stopped = OnlineStatus.OFFLINE;
}

@Comment("True if all guild members should be cached, in turn allowing @mentions\n"
+ "NB: This requires the Privileged Gateway Intent 'Server Members' to be enabled on your Discord bot!")
@RequiresRestart
public boolean cacheMembers = false;
}
@@ -0,0 +1,33 @@
package me.axieum.mcmod.minecord.impl.config;

import java.util.HashMap;
import java.util.Map;

import me.shedaniel.autoconfig.ConfigData;
import me.shedaniel.autoconfig.annotation.Config;
import me.shedaniel.autoconfig.annotation.ConfigEntry.Gui.RequiresRestart;
import me.shedaniel.cloth.clothconfig.shadowed.blue.endless.jankson.Comment;

import net.minecraft.util.Language;

@Config(name = "i18n")
public class I18nConfig implements ConfigData
{
@Comment("The language code used to load translations from")
@RequiresRestart
public String lang = Language.DEFAULT_LANGUAGE;

@Comment("A mapping of Minecraft dimension IDs to their respective names")
public Map<String, String> worlds = new HashMap<>(Map.ofEntries(
Map.entry("minecraft:overworld", "Overworld"),
Map.entry("minecraft:the_nether", "Nether"),
Map.entry("minecraft:the_end", "The End")
));

@Comment("A mapping of advancement types to their respective names")
public Map<String, String> advancementTypes = new HashMap<>(Map.ofEntries(
Map.entry("task", "task"),
Map.entry("challenge", "challenge"),
Map.entry("goal", "goal")
));
}
@@ -1,62 +1,36 @@
package me.axieum.mcmod.minecord.impl.config;

import me.shedaniel.autoconfig.AutoConfig;
import me.shedaniel.autoconfig.ConfigData;
import me.shedaniel.autoconfig.ConfigHolder;
import me.shedaniel.autoconfig.annotation.Config;
import me.shedaniel.autoconfig.annotation.ConfigEntry.Category;
import me.shedaniel.autoconfig.annotation.ConfigEntry.Gui.RequiresRestart;
import me.shedaniel.autoconfig.serializer.ConfigSerializer;
import me.shedaniel.autoconfig.serializer.JanksonConfigSerializer;
import me.shedaniel.cloth.clothconfig.shadowed.blue.endless.jankson.Comment;
import net.dv8tion.jda.api.OnlineStatus;
import me.shedaniel.autoconfig.serializer.PartitioningSerializer;

import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;

@Config(name = "minecord/bot")
public class MinecordConfig implements ConfigData
@Config(name = "minecord")
public class MinecordConfig extends PartitioningSerializer.GlobalData
{
@Comment("Token used to authenticate against your Discord bot")
@RequiresRestart
public String token = "";
@Category("bot")
public BotConfig bot = new BotConfig();

@Category("Bot Status")
@Comment("Bot statuses relayed during the lifecycle of the server")
public StatusSchema status = new StatusSchema();

/**
* Bot status configuration schema.
*/
public static class StatusSchema
{
@Comment("Status while the server is starting")
public OnlineStatus starting = OnlineStatus.IDLE;

@Comment("Status after the server has started")
public OnlineStatus started = OnlineStatus.ONLINE;

@Comment("Status while the server is stopping")
public OnlineStatus stopping = OnlineStatus.DO_NOT_DISTURB;

@Comment("Status after the server has stopped")
public OnlineStatus stopped = OnlineStatus.OFFLINE;
}

@Comment("True if all guild members should be cached, in turn allowing @mentions\n"
+ "NB: This requires the Privileged Gateway Intent 'Server Members' to be enabled on your Discord bot!")
@RequiresRestart
public boolean cacheMembers = false;
@Category("i18n")
public I18nConfig i18n = new I18nConfig();

/**
* Registers and prepares a new configuration instance.
*
* @return registered config holder
* @see me.shedaniel.autoconfig.AutoConfig#register(Class, ConfigSerializer.Factory)
* @see AutoConfig#register(Class, ConfigSerializer.Factory)
*/
public static ConfigHolder<MinecordConfig> init()
{
// Register the config
ConfigHolder<MinecordConfig> holder = AutoConfig.register(MinecordConfig.class, JanksonConfigSerializer::new);
ConfigHolder<MinecordConfig> holder = AutoConfig.register(
MinecordConfig.class, PartitioningSerializer.wrap(JanksonConfigSerializer::new)
);

// Listen for when the server is reloading (i.e. /reload), and reload the config
ServerLifecycleEvents.START_DATA_PACK_RELOAD.register((s, m) ->
Expand Down

0 comments on commit 64b95c0

Please sign in to comment.