traitHandlers;
@@ -30,6 +41,7 @@ public final class DynmapCitizens extends JavaPlugin {
     @Override
     public void onEnable() {
         DynmapCitizens.instance = this;
+
         //Initialize quest and dynmap APIs
         PluginManager pluginManager = Bukkit.getPluginManager();
         Plugin dynmapPlugin = pluginManager.getPlugin("dynmap");
@@ -38,7 +50,7 @@ public final class DynmapCitizens extends JavaPlugin {
             this.onDisable();
             return;
         }
-        this.dynmapAPI = dynmapAPI;
+        this.dynmapAPIInstance = dynmapAPI;
 
         this.globalSettings = new GlobalSettings();
         FileConfiguration configuration = this.getConfig();
@@ -49,6 +61,14 @@ public final class DynmapCitizens extends JavaPlugin {
         configuration = this.getConfig();
         this.globalSettings.load(configuration);
 
+        //Load all messages
+        translator = new Translator();
+        translator.registerMessageCategory(TranslatableTimeUnit.UNIT_SECOND);
+        translator.registerMessageCategory(QuestsTranslatableMessage.QUESTS_REQUIREMENTS_FORMAT);
+        translator.registerMessageCategory(SentinelTranslatableMessage.SENTINEL_DESCRIPTION);
+        translator.loadLanguages(this.getDataFolder(), "en", "en");
+        stringFormatter = new StringFormatter(this.getDescription().getName(), translator);
+
         //Initialize all enabled traits
         initializeTraitHandlers(configuration);
 
@@ -69,6 +89,24 @@ public final class DynmapCitizens extends JavaPlugin {
         //TODO: Perhaps remove icons, just in case?
     }
 
+    /**
+     * Gets the translator to use for translation
+     *
+     * @return The translator to use
+     */
+    public static Translator getTranslator() {
+        return translator;
+    }
+
+    /**
+     * Gets the string formatter to use for formatting
+     *
+     * @return The string formatter to use
+     */
+    public static StringFormatter getFormatter() {
+        return stringFormatter;
+    }
+
     /**
      * Gets the global settings for this plugin
      *
@@ -93,7 +131,7 @@ public final class DynmapCitizens extends JavaPlugin {
      * @return A reference to the Dynmap API
      */
     public DynmapAPI getDynmapAPI() {
-        return this.dynmapAPI;
+        return this.dynmapAPIInstance;
     }
 
     /**
@@ -117,6 +155,7 @@ public final class DynmapCitizens extends JavaPlugin {
         this.traitHandlers.add(new QuestsHandler());
         this.traitHandlers.add(new SentinelHandler());
         this.traitHandlers.add(new MinstrelHandler());
+        this.traitHandlers.add(new DTLTradersHandler());
 
         //Load and initialize all enabled trait handlers
         for (CitizensTraitHandler handler : this.traitHandlers) {
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/formatting/QuestsTranslatableMessage.java b/src/main/java/net/knarcraft/dynmapcitizens/formatting/QuestsTranslatableMessage.java
new file mode 100644
index 0000000..1b56b61
--- /dev/null
+++ b/src/main/java/net/knarcraft/dynmapcitizens/formatting/QuestsTranslatableMessage.java
@@ -0,0 +1,169 @@
+package net.knarcraft.dynmapcitizens.formatting;
+
+import net.knarcraft.knarlib.formatting.TranslatableMessage;
+
+/**
+ * An enum describing all of DynmapCitizens' translatable messages
+ */
+public enum QuestsTranslatableMessage implements TranslatableMessage {
+
+    /**
+     * The format for a quest's planner description
+     *
+     * Placeholders: {questCoolDown}, {questFrom}, {questUntil}, {questRepeat}
+     */
+    QUESTS_PLANNER_DESCRIPTION,
+
+    /**
+     * The format for a quest's cool-down
+     *
+     * Placeholders: {coolDown}
+     */
+    QUESTS_PLANNER_COOL_DOWN,
+
+    /**
+     * The text to display if a quest cannot be repeated
+     */
+    QUESTS_PLANNER_UNREPEATABLE,
+
+    /**
+     * The format for a quest's first availability date
+     *
+     * Placeholders: {startDate}
+     */
+    QUESTS_PLANNER_FROM,
+
+    /**
+     * The format for a quest's last availability date
+     *
+     * Placeholders: {endDate}
+     */
+    QUESTS_PLANNER_UNTIL,
+
+    /**
+     * The format for a quest's repeat delay
+     *
+     * Placeholders: {repeatDelay}
+     */
+    QUEST_PLANNER_REPEAT,
+
+    /**
+     * The format for a quest's requirements
+     *
+     * Placeholders: {requirementQuestPoints}, {requirementExp}, {requirementBlockedByQuests},
+     * {requirementRequiredQuests}, {requirementRequiredItems}, {requirementMCMMOSkills}, {requirementPermissions},
+     * {requirementCustom}
+     */
+    QUESTS_REQUIREMENTS_FORMAT,
+
+    /**
+     * The format for a quest's quest point requirement
+     *
+     * Placeholders: {questPoints}
+     */
+    QUESTS_REQUIREMENTS_QUEST_POINTS,
+
+    /**
+     * The format for a quest's exp requirement
+     *
+     * Placeholders: {exp}
+     */
+    QUESTS_REQUIREMENTS_EXP,
+
+    /**
+     * The format for a quest's blocking quests
+     *
+     * Placeholders: {blockingQuests}
+     */
+    QUESTS_REQUIREMENTS_BLOCKED_BY_QUEST_FORMAT,
+
+    /**
+     * The format for one of a quest's blocking quests
+     *
+     * Placeholders: {questName}
+     */
+    QUESTS_REQUIREMENTS_BLOCKED_BY_QUEST_ITEM,
+
+    /**
+     * The format for a quest's required quests
+     *
+     * Placeholders: {requiredQuests}
+     */
+    QUESTS_REQUIREMENTS_REQUIRED_QUEST_FORMAT,
+
+    /**
+     * The format for one of a quest's required quests
+     *
+     * Placeholders: {questName}
+     */
+    QUESTS_REQUIREMENTS_REQUIRED_QUEST_ITEM,
+
+    /**
+     * The format for a quest's required items
+     *
+     * Placeholders: {requiredItems}
+     */
+    QUESTS_REQUIREMENTS_REQUIRED_ITEM_FORMAT,
+
+    /**
+     * The format for one of a quest's required items
+     *
+     * Placeholders: {itemName}
+     */
+    QUESTS_REQUIREMENTS_REQUIRED_ITEM_ITEM,
+
+    /**
+     * The format for a quest's mcMMO skill requirement
+     *
+     * Placeholders: {skill}, {level}
+     */
+    QUESTS_REQUIREMENTS_MC_MMO_SKILL,
+
+    /**
+     * The format for a quest's required permissions
+     *
+     * Placeholders: {permissions}
+     */
+    QUESTS_REQUIREMENTS_REQUIRED_PERMISSION_FORMAT,
+
+    /**
+     * The format for one of a quest's required permissions
+     *
+     * Placeholders: {permission}
+     */
+    QUESTS_REQUIREMENTS_REQUIRED_PERMISSION_ITEM,
+
+    /**
+     * The format for a quest's reach area name
+     *
+     * Placeholders: {name}
+     */
+    QUESTS_REACH_AREA_NAME_FORMAT,
+
+    /**
+     * The format for a quest's reach area's description
+     *
+     * Placeholders: {areaName}, {questName}
+     */
+    QUESTS_REACH_AREA_DESCRIPTION_FORMAT,
+
+    /**
+     * The format for a quest's kill area name
+     *
+     * Placeholders: {name}
+     */
+    QUESTS_KILL_AREA_NAME_FORMAT,
+
+    /**
+     * The format for a quest's kill area description
+     *
+     * Placeholders: {areaName}, {questName}, {mobName}, {mobAmount}
+     */
+    QUESTS_KILL_AREA_DESCRIPTION_FORMAT,
+    ;
+
+    @Override
+    public TranslatableMessage[] getAllMessages() {
+        return QuestsTranslatableMessage.values();
+    }
+}
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/formatting/SentinelTranslatableMessage.java b/src/main/java/net/knarcraft/dynmapcitizens/formatting/SentinelTranslatableMessage.java
new file mode 100644
index 0000000..27898cc
--- /dev/null
+++ b/src/main/java/net/knarcraft/dynmapcitizens/formatting/SentinelTranslatableMessage.java
@@ -0,0 +1,31 @@
+package net.knarcraft.dynmapcitizens.formatting;
+
+import net.knarcraft.knarlib.formatting.TranslatableMessage;
+
+/**
+ * An enum describing all translatable messages for sentinels
+ */
+public enum SentinelTranslatableMessage implements TranslatableMessage {
+
+    /**
+     * The format for the basic description of any sentinel
+     *
+     * Placeholders: {name}, {squad}, {sentinelDetails}
+     */
+    SENTINEL_DESCRIPTION,
+
+    /**
+     * The format for the detailed description of any sentinel
+     *
+     * Placeholders: {invincible}, {armor}, {health}, {accuracy}, {damage}, {speed}, {allowKnockback}, {range},
+     * {reach}, {targets}, {avoids},  {ignores}
+     */
+    SENTINEL_DETAILS,
+    ;
+
+    @Override
+    public TranslatableMessage[] getAllMessages() {
+        return SentinelTranslatableMessage.values();
+    }
+
+}
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/DTLTradersHandler.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/DTLTradersHandler.java
new file mode 100644
index 0000000..9657fda
--- /dev/null
+++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/DTLTradersHandler.java
@@ -0,0 +1,53 @@
+package net.knarcraft.dynmapcitizens.handler.trait;
+
+import net.citizensnpcs.api.CitizensAPI;
+import net.citizensnpcs.api.npc.NPC;
+import net.citizensnpcs.api.trait.Trait;
+import net.knarcraft.dynmapcitizens.DynmapCitizens;
+import net.knarcraft.dynmapcitizens.property.Icon;
+import net.knarcraft.dynmapcitizens.settings.DTLTradersSettings;
+import net.knarcraft.dynmapcitizens.settings.TraitSettings;
+import org.dynmap.markers.GenericMarker;
+
+/**
+ * A handler class for the minstrel trait
+ */
+public class DTLTradersHandler extends AbstractTraitHandler {
+
+    private final DTLTradersSettings settings = new DTLTradersSettings();
+
+    @Override
+    public void initialize() {
+        super.isEnabled = false;
+        CitizensAPI.getTraitFactory().getRegisteredTraits().forEach(traitInfo -> {
+            if (traitInfo.getTraitName().equals("trader")) {
+                super.isEnabled = true;
+            }
+        });
+
+        if (this.isEnabled) {
+            super.initializeMarkerSet();
+        }
+    }
+
+    @Override
+    public TraitSettings getSettings() {
+        return this.settings;
+    }
+
+    @Override
+    public void updateMarkers() {
+        //Remove existing markers
+        super.markerSet.getMarkers().forEach(GenericMarker::deleteMarker);
+
+        Class extends Trait> traderTrait = CitizensAPI.getTraitFactory().getTraitClass("trader");
+        for (NPC npc : CitizensAPI.getNPCRegistry()) {
+            if (npc.hasTrait(traderTrait)) {
+                String description = "" + npc.getName() + "
";
+                addNPCMarker(npc.getUniqueId(), "Trader NPC: ", description,
+                        DynmapCitizens.getInstance().getGlobalSettings().getMarkerIcons().get(Icon.TRADER), super.markerSet);
+            }
+        }
+    }
+
+}
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/SentinelHandler.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/SentinelHandler.java
index 28a7993..dc0938e 100644
--- a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/SentinelHandler.java
+++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/SentinelHandler.java
@@ -47,12 +47,14 @@ public class SentinelHandler extends AbstractTraitHandler {
                     description += "
Squad: " + trait.squad;
                 }
                 if (settings.displaySentinelStats()) {
-                    description += "
Invincible: " + trait.invincible + "
Armor: " +
-                            trait.armor + "
Health: " + trait.health + "
Accuracy: " + trait.accuracy +
-                            "
Damage: " + trait.damage + "
Allow knockback: " + trait.allowKnockback;
+                    description += "
Invincible: " + trait.invincible + "
Armor: " + trait.armor;
+                    description += "
Health: " + trait.health + "
Accuracy: " + trait.accuracy;
+                    description += "
Damage: " + trait.damage + "
Speed: " + trait.speed;
+                    description += "
Allow knockback: " + trait.allowKnockback;
                     description += "
Range: " + trait.range + "
Reach: " + trait.reach;
-                    description += "
Targets: " + trait.allTargets.toAllInOneString() + "
Avoids: " +
-                            trait.allAvoids.toAllInOneString() + "
Ignores: " + trait.allIgnores.toAllInOneString();
+                    description += "
Targets: " + trait.allTargets.toAllInOneString();
+                    description += "
Avoids: " + trait.allAvoids.toAllInOneString();
+                    description += "
Ignores: " + trait.allIgnores.toAllInOneString();
                 }
                 addNPCMarker(npc.getUniqueId(), "Sentinel NPC: ", description,
                         DynmapCitizens.getInstance().getGlobalSettings().getMarkerIcons().get(Icon.SENTINEL), super.markerSet);
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestAreaHandler.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestAreaHandler.java
index 25cf000..adf80b8 100644
--- a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestAreaHandler.java
+++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestAreaHandler.java
@@ -3,9 +3,13 @@ package net.knarcraft.dynmapcitizens.handler.trait.quests;
 import me.blackvein.quests.QuestsAPI;
 import me.blackvein.quests.quests.IQuest;
 import me.blackvein.quests.quests.IStage;
+import net.knarcraft.dynmapcitizens.DynmapCitizens;
 import net.knarcraft.dynmapcitizens.settings.QuestsSettings;
 import net.knarcraft.dynmapcitizens.util.DynmapHelper;
 import net.knarcraft.dynmapcitizens.util.QuestsHelper;
+import net.knarcraft.knarlib.formatting.StringFormatter;
+import net.knarcraft.knarlib.formatting.StringReplacer;
+import net.knarcraft.knarlib.formatting.Translator;
 import org.bukkit.Location;
 import org.bukkit.entity.EntityType;
 import org.dynmap.DynmapAPI;
@@ -14,6 +18,11 @@ import org.dynmap.markers.MarkerSet;
 
 import java.util.List;
 
+import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_KILL_AREA_DESCRIPTION_FORMAT;
+import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_KILL_AREA_NAME_FORMAT;
+import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REACH_AREA_DESCRIPTION_FORMAT;
+import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REACH_AREA_NAME_FORMAT;
+
 /**
  * A handler class for quest areas
  */
@@ -24,6 +33,7 @@ public class QuestAreaHandler {
     private final MarkerSet reachAreaMarkerSet;
     private final QuestsSettings settings;
     private final List unavailableQuests;
+    private final StringFormatter formatter;
 
     /**
      * Instantiates a new quest area handler
@@ -38,6 +48,7 @@ public class QuestAreaHandler {
         this.questsAPI = questsAPI;
         this.settings = settings;
         this.unavailableQuests = unavailableQuests;
+        this.formatter = DynmapCitizens.getFormatter();
         killAreaMarkerSet = DynmapHelper.initializeMarkerSet(dynmapAPI, settings.getKillAreaSettings());
         reachAreaMarkerSet = DynmapHelper.initializeMarkerSet(dynmapAPI, settings.getReachAreaSettings());
     }
@@ -75,11 +86,16 @@ public class QuestAreaHandler {
             Location location = stage.getLocationsToReach().get(i);
             int radius = stage.getRadiiToReachWithin().get(i);
             String areaName = stage.getLocationNames().get(i);
-            String description = "";
+
+            String formattedAreaName;
             if (areaName != null) {
-                description += "" + areaName + "
";
+                formattedAreaName = formatter.replacePlaceholder(QUESTS_REACH_AREA_NAME_FORMAT, "{name}", areaName);
+            } else {
+                formattedAreaName = "";
             }
-            description += "Target location for " + quest.getName();
+
+            String description = formatter.replacePlaceholders(QUESTS_REACH_AREA_DESCRIPTION_FORMAT,
+                    new String[]{"{areaName}", "{questName}"}, new String[]{formattedAreaName, quest.getName()});
             DynmapHelper.markLocation(location, radius, description, reachAreaMarkerSet, settings.getReachAreaSettings());
         }
     }
@@ -101,13 +117,20 @@ public class QuestAreaHandler {
             int mobAmount = stage.getMobNumToKill().get(i);
             String areaName = stage.getKillNames().get(i);
 
-            String description = "";
+            String formattedAreaName;
             if (areaName != null) {
-                description += "" + areaName + "
";
+                formattedAreaName = formatter.replacePlaceholder(QUESTS_KILL_AREA_NAME_FORMAT, "{name}", areaName);
+            } else {
+                formattedAreaName = "";
             }
-            description += "Kill location for " + quest.getName() +
-                    "
Kill " + QuestsHelper.normalizeName(mob.name()) + " x " + mobAmount;
-            DynmapHelper.markLocation(location, radius, description, killAreaMarkerSet, settings.getKillAreaSettings());
+
+            Translator translator = DynmapCitizens.getTranslator();
+            StringReplacer replacer = new StringReplacer(translator.getTranslatedMessage(QUESTS_KILL_AREA_DESCRIPTION_FORMAT));
+            replacer.add("{areaName}", formattedAreaName);
+            replacer.add("{questName}", quest.getName());
+            replacer.add("{mobName}", QuestsHelper.normalizeName(mob.name()));
+            replacer.add("{mobAmount}", String.valueOf(mobAmount));
+            DynmapHelper.markLocation(location, radius, replacer.replace(), killAreaMarkerSet, settings.getKillAreaSettings());
         }
     }
 
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestPlannerInfoGenerator.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestPlannerInfoGenerator.java
index 3d6d6e5..d3bce69 100644
--- a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestPlannerInfoGenerator.java
+++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestPlannerInfoGenerator.java
@@ -2,7 +2,12 @@ package net.knarcraft.dynmapcitizens.handler.trait.quests;
 
 import me.blackvein.quests.quests.IQuest;
 import me.blackvein.quests.quests.Planner;
-import net.knarcraft.dynmapcitizens.util.TimeFormatter;
+import net.knarcraft.dynmapcitizens.DynmapCitizens;
+import net.knarcraft.knarlib.formatting.TimeFormatter;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
 
 /**
  * A class to generate a string containing all information about a quest's planner info
@@ -33,7 +38,8 @@ public class QuestPlannerInfoGenerator {
         //Quest can be repeated after a cool-down
         if (planner.hasCooldown()) {
             plannerInfo.append("Quest repeatable after: ");
-            plannerInfo.append(TimeFormatter.getDurationString(planner.getCooldown() / 1000));
+            plannerInfo.append(TimeFormatter.getDurationString(DynmapCitizens.getTranslator(),
+                    planner.getCooldown() / 1000));
             plannerInfo.append("");
         } else {
             plannerInfo.append("Quest cannot be repeated!");
@@ -42,23 +48,36 @@ public class QuestPlannerInfoGenerator {
         //Quest only becomes available after the start date
         if (planner.hasStart()) {
             plannerInfo.append("Quest available from ");
-            plannerInfo.append(TimeFormatter.formatTimestamp(planner.getStartInMillis())).append("");
+            plannerInfo.append(formatTimestamp(planner.getStartInMillis())).append("");
         }
 
         //Quest is only available until the end date
         if (planner.hasEnd()) {
             plannerInfo.append("Quest available until ");
-            plannerInfo.append(TimeFormatter.formatTimestamp(planner.getEndInMillis())).append("");
+            plannerInfo.append(formatTimestamp(planner.getEndInMillis())).append("");
         }
 
         //Quest availability repeats
         if (planner.hasRepeat()) {
             plannerInfo.append("Quest will become available again after ");
-            plannerInfo.append(TimeFormatter.getDurationString(planner.getRepeat() / 1000)).append("");
+            plannerInfo.append(TimeFormatter.getDurationString(DynmapCitizens.getTranslator(),
+                    planner.getRepeat() / 1000)).append("");
         }
 
         plannerInfo.append("");
         return plannerInfo.toString();
     }
 
+    /**
+     * Gets a datetime string for the given timestamp
+     *
+     * @param timestamp A timestamp in milliseconds
+     * @return A datetime string
+     */
+    private String formatTimestamp(long timestamp) {
+        DateFormat format = new SimpleDateFormat("dd MM yyyy HH:mm:ss");
+        Date date = new Date(timestamp);
+        return format.format(date);
+    }
+
 }
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestRequirementsInfoGenerator.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestRequirementsInfoGenerator.java
index 426b2ff..03f2dc0 100644
--- a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestRequirementsInfoGenerator.java
+++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestRequirementsInfoGenerator.java
@@ -1,27 +1,51 @@
 package net.knarcraft.dynmapcitizens.handler.trait.quests;
 
+import me.blackvein.quests.QuestsAPI;
 import me.blackvein.quests.quests.IQuest;
 import me.blackvein.quests.quests.Requirements;
+import net.knarcraft.dynmapcitizens.DynmapCitizens;
 import net.knarcraft.dynmapcitizens.util.QuestsHelper;
+import net.knarcraft.knarlib.formatting.StringFormatter;
+import net.knarcraft.knarlib.formatting.StringReplacer;
+import net.knarcraft.knarlib.formatting.TranslatableMessage;
 import org.bukkit.inventory.ItemStack;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
+import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_BLOCKED_BY_QUEST_FORMAT;
+import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_BLOCKED_BY_QUEST_ITEM;
+import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_EXP;
+import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_FORMAT;
+import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_MC_MMO_SKILL;
+import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_QUEST_POINTS;
+import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_REQUIRED_ITEM_FORMAT;
+import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_REQUIRED_ITEM_ITEM;
+import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_REQUIRED_PERMISSION_FORMAT;
+import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_REQUIRED_PERMISSION_ITEM;
+import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_REQUIRED_QUEST_FORMAT;
+import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_REQUIRED_QUEST_ITEM;
+
 /**
  * A class to generate a string containing all information about a quest's requirements
  */
 public class QuestRequirementsInfoGenerator {
 
+    private final QuestsAPI questsAPI;
     private final IQuest quest;
+    private final StringFormatter formatter;
 
     /**
      * Instantiates a new quest requirement info generator
      *
-     * @param quest The quest to generate information about
+     * @param questsAPI The API to use for getting quest information
+     * @param quest     The quest to generate information about
      */
-    public QuestRequirementsInfoGenerator(IQuest quest) {
+    public QuestRequirementsInfoGenerator(QuestsAPI questsAPI, IQuest quest) {
+        this.questsAPI = questsAPI;
         this.quest = quest;
+        formatter = DynmapCitizens.getFormatter();
     }
 
     /**
@@ -31,78 +55,140 @@ public class QuestRequirementsInfoGenerator {
      */
     public String getQuestRequirementsInfo() {
         Requirements requirements = quest.getRequirements();
-        StringBuilder requirementInfo = new StringBuilder();
         if (!requirements.hasRequirement()) {
-            return requirementInfo.toString();
+            return "";
         }
 
-        requirementInfo.append("Requirements: ");
+        StringReplacer replacer = new StringReplacer(DynmapCitizens.getTranslator().getTranslatedMessage(
+                QUESTS_REQUIREMENTS_FORMAT));
 
-        if (requirements.getQuestPoints() > 0) {
-            requirementInfo.append("- ").append(requirements.getQuestPoints()).append(" quest points
 ");
-        }
+        //Add info about quest point requirement
+        replacer.add("{requirementQuestPoints}", requirements.getQuestPoints() > 0 ?
+                formatter.replacePlaceholder(QUESTS_REQUIREMENTS_QUEST_POINTS,
+                        "{questPoints}", String.valueOf(requirements.getQuestPoints())) : "");
 
-        if (requirements.getExp() > 0) {
-            requirementInfo.append("- ").append(requirements.getExp()).append(" exp
 ");
-        }
+        //Add info about exp requirement
+        replacer.add("{requirementExp}", requirements.getExp() > 0 ? formatter.replacePlaceholder(
+                QUESTS_REQUIREMENTS_EXP, "{exp}", String.valueOf(requirements.getExp())) : "");
 
-        if (!requirements.getBlockQuests().isEmpty()) {
-            requirementInfo.append("- Blocked by quests:
");
-            for (IQuest blockQuest : requirements.getBlockQuests()) {
-                requirementInfo.append("- ").append(blockQuest.getName()).append("
 ");
-            }
-            requirementInfo.append("
 ");
-        }
+        //Add info about blocking quests
+        replacer.add("{requirementBlockedByQuests}", !requirements.getBlockQuestIds().isEmpty() ?
+                getRequirementList(getQuestNames(requirements.getBlockQuestIds()),
+                        QUESTS_REQUIREMENTS_BLOCKED_BY_QUEST_FORMAT, "{blockingQuests}",
+                        QUESTS_REQUIREMENTS_BLOCKED_BY_QUEST_ITEM, "{questName}") : "");
 
-        if (!requirements.getNeededQuests().isEmpty()) {
-            requirementInfo.append("- Required quests:
");
-            for (IQuest neededQuest : requirements.getNeededQuests()) {
-                requirementInfo.append("- ").append(neededQuest.getName()).append("
 ");
-            }
-            requirementInfo.append("
 ");
-        }
+        //Add info about required quests
+        replacer.add("{requirementRequiredQuests}", !requirements.getBlockQuestIds().isEmpty() ?
+                getRequirementList(getQuestNames(requirements.getBlockQuestIds()),
+                        QUESTS_REQUIREMENTS_REQUIRED_QUEST_FORMAT, "{requiredQuests}",
+                        QUESTS_REQUIREMENTS_REQUIRED_QUEST_ITEM, "{questName}") : "");
 
-        if (!requirements.getItems().isEmpty()) {
-            requirementInfo.append("- Required items:
");
-            for (ItemStack item : requirements.getItems()) {
-                requirementInfo.append("- ").append(QuestsHelper.getUpperCasedItemStackString(item)).append("
 ");
-            }
-            requirementInfo.append("
 ");
-        }
+        //Add info about required items
+        replacer.add("{requirementRequiredItems}", !requirements.getItems().isEmpty() ?
+                getRequirementList(getItemNames(requirements.getItems()), QUESTS_REQUIREMENTS_REQUIRED_ITEM_FORMAT,
+                        "{requiredItems}", QUESTS_REQUIREMENTS_REQUIRED_ITEM_ITEM, "{itemName}") : "");
 
+        //Add info about required mcMMO skills
         if (!requirements.getMcmmoSkills().isEmpty()) {
             List skills = requirements.getMcmmoSkills();
             List amounts = requirements.getMcmmoAmounts();
+            StringBuilder mcMMOSkillsBuilder = new StringBuilder();
             for (int i = 0; i < skills.size(); i++) {
-                requirementInfo.append("- Requires mcMMO skill ").append(skills.get(i)).append(" at level ");
-                requirementInfo.append(amounts.get(i)).append("
 ");
+                mcMMOSkillsBuilder.append(formatter.replacePlaceholders(QUESTS_REQUIREMENTS_MC_MMO_SKILL, new String[]{
+                        "{skill}", "{level}"}, new String[]{skills.get(i), String.valueOf(amounts.get(i))}));
             }
+            replacer.add("{requirementMCMMOSkills}", mcMMOSkillsBuilder.toString());
+        } else {
+            replacer.add("{requirementMCMMOSkills}", "");
         }
 
-        if (!requirements.getPermissions().isEmpty()) {
-            requirementInfo.append("- Required permissions:
");
-            for (String permission : requirements.getPermissions()) {
-                requirementInfo.append("- ").append(permission).append("
 ");
-            }
-            requirementInfo.append("
 ");
-        }
+        //Add info about required permissions
+        replacer.add("{requirementPermissions}", !requirements.getPermissions().isEmpty() ?
+                getRequirementList(requirements.getPermissions(), QUESTS_REQUIREMENTS_REQUIRED_PERMISSION_FORMAT,
+                        "{permissions}", QUESTS_REQUIREMENTS_REQUIRED_PERMISSION_ITEM, "{permission}") : "");
 
         Map> customRequirementPlugins = requirements.getCustomRequirements();
+        StringBuilder customRequirementsBuilder = new StringBuilder();
         for (String plugin : customRequirementPlugins.keySet()) {
-            requirementInfo.append("- ").append(plugin).append(":
");
+            customRequirementsBuilder.append("- ").append(plugin).append(":
");
             //Note: The format of custom requirements is kind of weird. First, you have the key for which plugin the 
             // requirement belongs to. Getting the value of the key gives another map. The map contains as key, the type
             // of value, like "Skill Amount" or "Skill Type". The value is the actual value of whatever it is.
             Map customRequirementEntry = customRequirementPlugins.get(plugin);
             for (String requirementDescription : customRequirementEntry.keySet()) {
-                requirementInfo.append("- ").append(requirementDescription).append(" ");
-                requirementInfo.append(customRequirementEntry.get(requirementDescription)).append("
 ");
+                customRequirementsBuilder.append("- ").append(requirementDescription).append(" ");
+                customRequirementsBuilder.append(customRequirementEntry.get(requirementDescription)).append("
 ");
             }
-            requirementInfo.append("
 ");
+            customRequirementsBuilder.append("
 ");
         }
+        replacer.add("{requirementCustom}", customRequirementsBuilder.toString());
+        return replacer.replace();
+    }
 
-        requirementInfo.append("
");
-        return requirementInfo.toString();
+    /**
+     * Gets a list of item names from the given list of items
+     *
+     * @param items The items to get the names of
+     * @return The names of the given items
+     */
+    private List getItemNames(List items) {
+        List itemNames = new ArrayList<>();
+        for (ItemStack itemStack : items) {
+            itemNames.add(QuestsHelper.getUpperCasedItemStackString(itemStack));
+        }
+        return itemNames;
+    }
+
+    /**
+     * Gets a list of the quest names for the given quests
+     *
+     * @param questIds The quests to get names for
+     * @return A list of quest names
+     */
+    private List getQuestNames(List questIds) {
+        List questNames = new ArrayList<>(questIds.size());
+        for (String questId : questIds) {
+            IQuest quest = getQuest(questId);
+            if (quest != null) {
+                questNames.add(quest.getName());
+            }
+        }
+        return questNames;
+    }
+
+    /**
+     * Gets the quest with the given id
+     *
+     * @param questId The id of the quest to get
+     * @return The quest, or null if not found
+     */
+    private IQuest getQuest(String questId) {
+        for (IQuest quest : questsAPI.getLoadedQuests()) {
+            if (quest.getId().equals(questId)) {
+                return quest;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Gets a string for the given list of requirements
+     *
+     * @param itemList          The items to display in the list of requirements
+     * @param formatMessage     The translatable message describing the list format
+     * @param formatPlaceholder The placeholder to replace with the list items
+     * @param itemMessage       The translatable message describing each item's format
+     * @param itemPlaceholder   The placeholder to replace with each item in the list
+     * @return The string corresponding to the given requirement list
+     */
+    private String getRequirementList(List itemList, TranslatableMessage formatMessage, String formatPlaceholder,
+                                      TranslatableMessage itemMessage, String itemPlaceholder) {
+        StringBuilder blockedBuilder = new StringBuilder();
+        for (Object requirements : itemList) {
+            blockedBuilder.append(formatter.replacePlaceholder(itemMessage, itemPlaceholder,
+                    String.valueOf(requirements)));
+        }
+        return formatter.replacePlaceholder(formatMessage, formatPlaceholder, blockedBuilder.toString());
     }
 
 }
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestStagesInfoGenerator.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestStagesInfoGenerator.java
index 3b9d25a..f91575d 100644
--- a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestStagesInfoGenerator.java
+++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestStagesInfoGenerator.java
@@ -94,6 +94,9 @@ public class QuestStagesInfoGenerator {
         for (UUID npcId : stage.getNpcsToKill()) {
             questInfo.append("Kill NPC ").append(registry.getByUniqueId(npcId).getName()).append("");
         }
+        for (UUID npcId : stage.getNpcsToInteract()) {
+            questInfo.append("Talk to ").append(registry.getByUniqueId(npcId).getName()).append("");
+        }
 
         questInfo.append(getQuestItemsTaskString(stage.getBlocksToBreak(), "Break ")).append("");
         questInfo.append(getQuestItemsTaskString(stage.getBlocksToCut(), "Cut ")).append("");
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestsHandler.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestsHandler.java
index 3b928a0..b68cf7c 100644
--- a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestsHandler.java
+++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestsHandler.java
@@ -163,7 +163,7 @@ public class QuestsHandler extends AbstractTraitHandler {
                 stringBuilder.append(new QuestRewardsInfoGenerator(quest).getQuestRewardsInfo());
             }
             if (settings.displayRequirementInfo()) {
-                stringBuilder.append(new QuestRequirementsInfoGenerator(quest).getQuestRequirementsInfo());
+                stringBuilder.append(new QuestRequirementsInfoGenerator(questsAPI, quest).getQuestRequirementsInfo());
             }
             if (settings.displayPlannerInfo()) {
                 stringBuilder.append(new QuestPlannerInfoGenerator(quest).getQuestPlannerInfo());
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/property/Icon.java b/src/main/java/net/knarcraft/dynmapcitizens/property/Icon.java
index b0b5ee0..5f2f949 100644
--- a/src/main/java/net/knarcraft/dynmapcitizens/property/Icon.java
+++ b/src/main/java/net/knarcraft/dynmapcitizens/property/Icon.java
@@ -43,6 +43,11 @@ public enum Icon {
     /**
      * An icon representing a minstrel NPC
      */
-    MINSTREL
+    MINSTREL,
+
+    /**
+     * An icon representing a trader NPC
+     */
+    TRADER,
 
 }
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/settings/DTLTradersSettings.java b/src/main/java/net/knarcraft/dynmapcitizens/settings/DTLTradersSettings.java
new file mode 100644
index 0000000..80b3593
--- /dev/null
+++ b/src/main/java/net/knarcraft/dynmapcitizens/settings/DTLTradersSettings.java
@@ -0,0 +1,20 @@
+package net.knarcraft.dynmapcitizens.settings;
+
+import org.bukkit.configuration.file.FileConfiguration;
+
+/**
+ * All settings for the minstrel trait
+ */
+public class DTLTradersSettings extends AbstractTraitSettings {
+
+    @Override
+    public void load(FileConfiguration configuration) {
+        super.load(configuration);
+    }
+
+    @Override
+    protected String getTraitConfigRoot() {
+        return "traits.trader";
+    }
+
+}
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/settings/GlobalSettings.java b/src/main/java/net/knarcraft/dynmapcitizens/settings/GlobalSettings.java
index a7fe753..47f70bd 100644
--- a/src/main/java/net/knarcraft/dynmapcitizens/settings/GlobalSettings.java
+++ b/src/main/java/net/knarcraft/dynmapcitizens/settings/GlobalSettings.java
@@ -82,6 +82,7 @@ public class GlobalSettings {
             case BLACKSMITH -> "hammer";
             case SENTINEL -> "shield";
             case MINSTREL -> "theater";
+            case TRADER -> "coins";
         };
     }
 
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/util/TimeFormatter.java b/src/main/java/net/knarcraft/dynmapcitizens/util/TimeFormatter.java
deleted file mode 100644
index 5ebc8ba..0000000
--- a/src/main/java/net/knarcraft/dynmapcitizens/util/TimeFormatter.java
+++ /dev/null
@@ -1,98 +0,0 @@
-package net.knarcraft.dynmapcitizens.util;
-
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import static net.knarcraft.blacksmith.formatting.StringFormatter.replacePlaceholder;
-
-/**
- * A helper class for time formatting
- */
-public class TimeFormatter {
-
-    /**
-     * Gets a datetime string for the given timestamp
-     *
-     * @param timestamp A timestamp in milliseconds
-     * @return A datetime string
-     */
-    public static String formatTimestamp(long timestamp) {
-        DateFormat format = new SimpleDateFormat("dd MM yyyy HH:mm:ss");
-        Date date = new Date(timestamp);
-        return format.format(date);
-    }
-
-    /**
-     * Gets the string used for displaying this sign's duration
-     *
-     * @return The string used for displaying this sign's duration
-     */
-    public static String getDurationString(long duration) {
-        if (duration == 0) {
-            return "immediately";
-        } else {
-            double minute = 60;
-            double hour = minute * 60;
-            double day = hour * 24;
-            double week = day * 7;
-            double month = day * 30;
-            double year = day * 365;
-            double decade = year * 10;
-
-            Map timeUnits = new HashMap<>();
-            timeUnits.put(decade, new String[]{"decade", "decades"});
-            timeUnits.put(year, new String[]{"year", "years"});
-            timeUnits.put(month, new String[]{"month", "months"});
-            timeUnits.put(week, new String[]{"week", "weeks"});
-            timeUnits.put(day, new String[]{"day", "days"});
-            timeUnits.put(hour, new String[]{"hour", "hours"});
-            timeUnits.put(minute, new String[]{"minute", "minutes"});
-            timeUnits.put(1D, new String[]{"second", "seconds"});
-
-            List sortedUnits = new ArrayList<>(timeUnits.keySet());
-            Collections.sort(sortedUnits);
-            Collections.reverse(sortedUnits);
-
-            for (Double unit : sortedUnits) {
-                if (duration / unit >= 1) {
-                    double units = round(duration / unit);
-                    return formatDurationString(units, timeUnits.get(unit)[units == 1 ? 0 : 1],
-                            (units * 10) % 10 == 0);
-                }
-            }
-            return formatDurationString(duration, "seconds", false);
-        }
-    }
-
-    /**
-     * Rounds a number to its last two digits
-     *
-     * @param number The number to round
-     * @return The rounded number
-     */
-    private static double round(double number) {
-        return Math.round(number * 100.0) / 100.0;
-    }
-
-    /**
-     * Formats a duration string
-     *
-     * @param duration            The duration to display
-     * @param translatableMessage The time unit to display
-     * @param castToInt           Whether to cast the duration to an int
-     * @return The formatted duration string
-     */
-    private static String formatDurationString(double duration, String translatableMessage, boolean castToInt) {
-        String durationFormat = "{duration} {unit}";
-        durationFormat = replacePlaceholder(durationFormat, "{unit}", translatableMessage);
-        return replacePlaceholder(durationFormat, "{duration}", castToInt ? String.valueOf((int) duration) :
-                String.valueOf(duration));
-    }
-
-}
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index 9dd6bdc..95ef02b 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -16,6 +16,8 @@ icon:
   SENTINEL: "shield"
   # The marker used for minstrels
   MINSTREL: "theater"
+  # The marker used for traders
+  TRADER: "coins"
 
 # Settings for how often markers will be updated
 timer:
@@ -129,4 +131,15 @@ traits:
     # Whether to hide the minstrel icon layer by default
     markersHiddenByDefault: false
     # Whether to display the list of songs a minstrel is playing
-    displayMinstrelSongs: true
\ No newline at end of file
+    displayMinstrelSongs: true
+  # Settings for the trader trait
+  trader:
+    enabled: true
+    # The priority of trader markers. Higher priority markers will display on top of lower priority ones
+    markerSetPriority: 1
+    # The id of the trader marker set. Change if it overlaps with an existing set id
+    markerSetId: "traders"
+    # The name of the trader marker set. Change it if you want a cooler name
+    markerSetName: "Traders"
+    # Whether to hide the trader icon layer by default
+    markersHiddenByDefault: false
\ No newline at end of file
diff --git a/src/main/resources/strings.yml b/src/main/resources/strings.yml
new file mode 100644
index 0000000..21803ac
--- /dev/null
+++ b/src/main/resources/strings.yml
@@ -0,0 +1,76 @@
+en:
+  SENTINEL_DESCRIPTION: |
+    {name}
+    
Squad: {squad}
+    {sentinelDetails}
+  SENTINEL_DETAILS: |
+    
+    - Invincible: {invincible}
 
+    - Armor: {armor}
 
+    - Health: {health}
 
+    - Accuracy: {accuracy}
 
+    - Damage: {damage}
 
+    - Speed: {speed}
 
+    - Allow knockback: {allowKnockback}
 
+    - Range: {range}
 
+    - Reach: {reach}
 
+    - Targets: {targets}
 
+    - Avoids: {avoids}
 
+    - Ignores: {ignores}
 
+    
+  QUESTS_PLANNER_DESCRIPTION: |
+    Planner:
+    {questCoolDown}
+    {questFrom}
+    {questUntil}
+    {questRepeat}
+    
+  QUESTS_PLANNER_COOL_DOWN: "Quest repeatable after: {coolDown}"
+  QUESTS_PLANNER_UNREPEATABLE: "Quest cannot be repeated!"
+  QUESTS_PLANNER_FROM: "Quest available from {startDate}"
+  QUESTS_PLANNER_UNTIL: "Quest available until {endDate}"
+  QUEST_PLANNER_REPEAT: "Quest will become available again after {repeatDelay}"
+  QUESTS_REQUIREMENTS_FORMAT: |
+    Requirements: 
+    {requirementQuestPoints}
+    {requirementExp}
+    {requirementBlockedByQuests}
+    {requirementRequiredQuests}
+    {requirementRequiredItems}
+    {requirementMCMMOSkills}
+    {requirementPermissions}
+    {requirementCustom}
+    
+  QUESTS_REQUIREMENTS_QUEST_POINTS: "{questPoints} quest points"
+  QUESTS_REQUIREMENTS_EXP: "{exp} exp"
+  QUESTS_REQUIREMENTS_BLOCKED_BY_QUEST_FORMAT: "Blocked by quests:"
+  QUESTS_REQUIREMENTS_BLOCKED_BY_QUEST_ITEM: "{questName}"
+  QUESTS_REQUIREMENTS_REQUIRED_QUEST_FORMAT: "Required quests:"
+  QUESTS_REQUIREMENTS_REQUIRED_QUEST_ITEM: "{questName}"
+  QUESTS_REQUIREMENTS_REQUIRED_ITEM_FORMAT: "Required items:"
+  QUESTS_REQUIREMENTS_REQUIRED_ITEM_ITEM: "{itemName}"
+  QUESTS_REQUIREMENTS_MC_MMO_SKILL: "Requires mcMMO skill {skill} at level {level}"
+  QUESTS_REQUIREMENTS_REQUIRED_PERMISSION_FORMAT: "Required permissions:"
+  QUESTS_REQUIREMENTS_REQUIRED_PERMISSION_ITEM: "{permission}"
+  QUESTS_REACH_AREA_NAME_FORMAT: "{name}
"
+  QUESTS_REACH_AREA_DESCRIPTION_FORMAT: "{areaName}Target location for {questName}"
+  QUESTS_KILL_AREA_NAME_FORMAT: "{name}
"
+  QUESTS_KILL_AREA_DESCRIPTION_FORMAT: "{areaName}Kill location for {questName}
Kill {mobName} x {mobAmount}"
+  DURATION_FORMAT: "in {time} {unit}"
+  UNIT_NOW: "imminently"
+  UNIT_SECOND: "second"
+  UNIT_SECONDS: "seconds"
+  UNIT_MINUTE: "minute"
+  UNIT_MINUTES: "minutes"
+  UNIT_HOUR: "hour"
+  UNIT_HOURS: "hours"
+  UNIT_DAY: "day"
+  UNIT_DAYS: "days"
+  UNIT_WEEK: "week"
+  UNIT_WEEKS: "weeks"
+  UNIT_MONTH: "month"
+  UNIT_MONTHS: "months"
+  UNIT_YEAR: "year"
+  UNIT_YEARS: "years"
+  UNIT_DECADE: "decade"
+  UNIT_DECADES: "decades"
\ No newline at end of file