<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: LukeZ</title>
    <description>The latest articles on DEV Community by LukeZ (@thelukez).</description>
    <link>https://dev.to/thelukez</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3267415%2Fa7be5d2b-1ad0-469b-aa9f-62d2153d98a6.png</url>
      <title>DEV Community: LukeZ</title>
      <link>https://dev.to/thelukez</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thelukez"/>
    <language>en</language>
    <item>
      <title>Building Logging V2 for Ticketon</title>
      <dc:creator>LukeZ</dc:creator>
      <pubDate>Sun, 19 Jul 2026 18:20:54 +0000</pubDate>
      <link>https://dev.to/thelukez/building-logging-v2-for-ticketon-d60</link>
      <guid>https://dev.to/thelukez/building-logging-v2-for-ticketon-d60</guid>
      <description>&lt;p&gt;&lt;em&gt;~15 minutes read time&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When being a Discord server admin/manager or moderator, it is important to know what happens in your server. Discord provides a basic Audit Log, which keeps track of the most important events like moderation actions, server/channel updates, role changes, and more.&lt;br&gt;&lt;br&gt;
This can be improved by using a bot like Sapphire (#NotAnAd) to log more events directly as messages in a channel of your choice, instead of having to go in the server settings and check the Audit Log - which is also limited to the last 90 days of events (or was it 30? I don't remember).&lt;/p&gt;

&lt;p&gt;However, many bots don't have logging features which track what happens to their own configs and actions - and if, then just stuff they do like moderation actions. But I can't count the number of times I had to answer questions like "How can I know who misconfigured the bot?" in the Sapphire support server. (One might think that I'm a fan of Sapphire)&lt;/p&gt;
&lt;h2&gt;
  
  
  The first logging system
&lt;/h2&gt;

&lt;p&gt;The first MVP of the Bot had a very basic logging system where I had an &lt;code&gt;AuditLogEntry&lt;/code&gt; mongoose model which was used to manually create documents when a ticket category was created/updated/deleted, a ticket created/closed/deleted, a custom message created/updated/deleted, and a few other events. This was enough to get started, but it was very limited and not very flexible.&lt;br&gt;&lt;br&gt;
For the MVP I also decided to just have an audit log page in the web dashboard where logs could be explored. The UI was also very ugly - mostly displaying the raw JSON data of the log entries. But it was enough to get started and to have a basic logging system in place.&lt;/p&gt;

&lt;p&gt;However, it was very messy as there would be two different places where logs could be generated: the web dashboard and the bot itself. For configuration changes, the web dashboard would need to compare the old and new documents, diff the fields - including nested fields - and generate a log entry. The result was a lot of code duplication and a lot of manually juggling the fields for every data model.&lt;/p&gt;
&lt;h2&gt;
  
  
  The new idea
&lt;/h2&gt;

&lt;p&gt;From the beginning I wanted to have a logging system that would also send logs to a Discord channel, so that server admins/mods could see what happens in their server without having to go to the web dashboard. This was also requested in December 2025 by a user in the support server in the &lt;code&gt;#suggestions&lt;/code&gt; channel.&lt;/p&gt;

&lt;p&gt;I then decided in early July 2026 to start working on a new logging system to unify the logging system and make it more flexible and easier to use. The idea was to have a single logging system that would handle all logs, from both the bot and from the web dashboard, and to have a single place where logs could be generated and sent to a Discord channel. This lead to a general architecture idea:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz5n6f6bqh8medj7xm68x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz5n6f6bqh8medj7xm68x.png" alt="First architecture idea" width="799" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now I asked myself, how do I make this work? Which technologies do I use? How do I make it scalable and maybe even be standalone from the bot and the web dashboard?&lt;/p&gt;

&lt;p&gt;My first idea was to have a Worker (a &lt;strong&gt;Cloudflare Worker&lt;/strong&gt; to be exact) that would have an API endpoint where the bot and the web dashboard could send log entries. It would also have a D1 database where all logs would be stored with a UUID (v7) primary key. The Worker would then have put logs in a Queue (Cloudflare Queue) as they come in and then process them sequentially, sending them to the Discord channel and storing them in the database.&lt;/p&gt;

&lt;p&gt;The requests to the worker would be secured by signing them with a secret key, and the worker would verify the signature before processing the request. This would ensure that only the bot and the web dashboard could send logs to the worker. This is a very common pattern for webhooks and is used by a number of my projects.&lt;/p&gt;

&lt;p&gt;Since I don't want the bot token to be in more places than necessary, I thought it would be best to have the worker send the logs to a Discord webhook instead of using the bot token. This would also allow for better ratelimit handling, as webhooks fall under a different ratelimit than bot tokens. The worker would then have a configuration for the webhook URL and the channel ID where logs should be sent.&lt;/p&gt;

&lt;p&gt;This issue that emerged from this idea was - how would the worker get the configuration for the webhook URL and the channel ID? The bot must do this and the worker must somehow access it. There were only two possible solutions: either the bot would have an API endpoint where the worker could request the webhook config or the worker would need to create it.&lt;/p&gt;

&lt;p&gt;However, I wanted to have a special feature which I saw in the Sapphire bot: Webhooks are created &lt;strong&gt;on demand&lt;/strong&gt; when they are needed. When there is an existing webhook, it would just reuse it. This would result in tons of requests to the bot to get or create webhooks, which would be a lot of overhead and would also require the bot to be online all the time.&lt;/p&gt;

&lt;p&gt;GDPR compliance was also a concern. The bot already does this in a regular cleanup process - the worker would have to clean up after its logs by its own. This would also either require the worker to request even more data from the bot or the bot sending the worker the data when something needs to be done.&lt;/p&gt;

&lt;p&gt;Phew. That was... quite a thought. This was so much to think about it was tiring me a lot. I decided to take a break and go to sleep. I would think about it tomorrow. I went to bed and had a good sleep.&lt;/p&gt;

&lt;p&gt;As I got up the next morning, I had a clear head and it suddenly all fell into place. My goal was &lt;strong&gt;unification&lt;/strong&gt; - A single writer/schema, kill the duplicated &lt;code&gt;AuditLog&lt;/code&gt; Mongoose model between the bot and web repo. Even though Cloudflare Workers is designed to scale, when there are hundreds of log events per minute, the sheer amount of requests between the bot, the worker and Discord would be so much overhead that it would mainly be a waste of resources.&lt;/p&gt;

&lt;p&gt;Then I tried a new approach. What if I just... not used Cloudflare Workers? Maybe integrate it (sort of) into the bot itself?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What I learned until now was: When you're building something, you should always set a goal and a vision for it. Sometimes take a step back and think about the bigger picture. Don't get lost in the details and don't be afraid to change your approach if you find a better one.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  A different approach
&lt;/h2&gt;

&lt;p&gt;Something else that was important was logging configs. After some discussing with our best friend Claude, I decided to also integrate the logging configs into the logging system in the bot - and not the dashboard.&lt;/p&gt;

&lt;p&gt;The board infrastructure would look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fef9sei08a3jinm2zeain.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fef9sei08a3jinm2zeain.png" alt="Final architecture idea" width="799" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;This architecture is just the backend part.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There are basically two possible emit sites: The &lt;strong&gt;bot&lt;/strong&gt; and the &lt;strong&gt;dashboard&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The bot would just call &lt;code&gt;log()&lt;/code&gt; to start a log entry. The dashboard backend would also call a &lt;code&gt;log()&lt;/code&gt; function to start a log entry - this one just sends it over to the bot via a REST API endpoint there (of which the bot already has a few). The Log Manager, the so-called &lt;code&gt;dispatcher&lt;/code&gt; would then handle the log event, save it in the DB and enqueue it to the Log Queue. The Log Queue would then process the log entry and send it to the Discord channel using the webhook-on-demand thing.&lt;br&gt;&lt;br&gt;
The log entry would be created in the database even before it is actually sent to Discord - not all logs are sent to Discord eventually, depending on the logging configuration.&lt;/p&gt;

&lt;p&gt;Everything could be done completely in existing infrastructure basically, as the secure communication from dashboard to bot is already done via the HTTP API in the bot. It would just be another endpoint.&lt;/p&gt;

&lt;p&gt;This whole system would also be kept "transport-agnostic" - meaning that the dispatcher and the &lt;code&gt;service.ts&lt;/code&gt; file (which is just being used to expose the logging functionality to the bot) could be extracted into a separate repo/package/process if needed.&lt;/p&gt;

&lt;p&gt;There is one downside though in comparison to the original idea of using a Cloudflare Worker: If the bot is down, the logging would also be down.&lt;br&gt;&lt;br&gt;
This is a tradeoff I am willing to make, as the dashboard configuration would still work - even if it has no effect because the bot can't use the new config at that moment. However, I have the idea of keeping unsuccessful log events from the dashboard in a queue and retrying them until the bot is back online - but this is a feature for the future. For now, I just want to get the logging system working and then improve it later. Maybe it's not even needed because it's not &lt;em&gt;that&lt;/em&gt; important because the dashboard only sends logs for configuration changes and not for core-functionality like ticket creation/closure/deletion, which is the most important part of the logging system. The core functionality is not given when the bot is down, so the logging system not being available in that time has no effect on the core functionality - because it doesn't exist anyways.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Routing Model
&lt;/h2&gt;

&lt;p&gt;In order to have the most customizability for the config, I wanted to be able to set a different channel for every log action type, but also group related events together and have a default channel for each group. This would allow for a more organized logging system, where similar events are logged in the same channel, while still allowing for specific events to be logged in their own channels if desired.&lt;/p&gt;

&lt;p&gt;After all, I decided to make the log config not be in a single document because... MongoDB is not relational and I wanted to have a more flexible and scalable solution, where I wouldn't need to add a new field to the model every time a new log action type is added. Instead, I structured the &lt;code&gt;LogConfig&lt;/code&gt; model so I could save one config document per action type. The key is a flattened string of the group and action type, for example &lt;code&gt;ticket.create.__default&lt;/code&gt; for the default channel of the &lt;code&gt;ticket.create&lt;/code&gt; group - &lt;code&gt;ticket.create.create&lt;/code&gt; for the specific channel for the &lt;code&gt;ticket.create&lt;/code&gt; action type. This way, I can easily add new action types without having to modify the model.&lt;/p&gt;

&lt;p&gt;The thing that I needed to figure out though was what to do with groups where there is only one action type. For example, the &lt;code&gt;ticket.create&lt;/code&gt; group only has one action type: &lt;code&gt;ticket.create.create&lt;/code&gt;. In this case, it would be redundant to have a group and an action type, as there is only one action type in the group. However, I wanted to keep the structure consistent and not have to special-case this situation - so the default channel for the group would just be unused in such cases. This would also allow for future expansion, where more action types could be added to the group without having to change the structure of the config.&lt;/p&gt;

&lt;p&gt;The channel in which the log will be sent after all is determined by first trying to figure out the specific channel ID (and if the log action type is even enabled), then falling back to the default channel for the group.&lt;/p&gt;

&lt;p&gt;One thing I gotta still figure out, is how to keep the taxonomy table (groups x action types) in sync between the dashboard and bot. I maybe make a submodule out of the most important things that need to stay in sync. The taxonomy table is basically like a vocabulary of all the log action types and their groups.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Dispatcher
&lt;/h2&gt;

&lt;p&gt;After I switched from the Cloudflare Workers idea to a more integrated approach, I still had one issue to solve: Queues.&lt;/p&gt;

&lt;p&gt;When there are a lot of log events happening in a short amount of time, the bot would be sending a lot of requests to Discord, which could quickly lead to ratelimits being hit. To somewhat mitigate this, I decided to implement an in-process per-channel buffer which groups up to 10 log events per channel and sends them in a single request to the Discord webhook. It uses a sliding window of 5 seconds to group log events together - if there are 3 events within 5 seconds for this channel, they will be sent together in a single message. If there are 10 events within 5 seconds, the queue will be flushed early. This way, I can reduce the number of requests to Discord and avoid hitting ratelimits.&lt;br&gt;&lt;br&gt;
This might seem weird - but if there are many close requests being executed at the same time in a server, they all will generate a log event.&lt;/p&gt;

&lt;p&gt;The dispatcher also handles retries for failed requests to Discord - or if the bot went down in the window between a log event creation and the actual sending of the log event. It's single-mechanism: only the sweep re-enqueues stuck events, running every minute. Each event carries an &lt;code&gt;attempts&lt;/code&gt; counter, bumped on every transient send failure; once an event hits 10 attempts it's marked &lt;code&gt;dead&lt;/code&gt; instead of being retried again. This is to avoid infinite retries in case of a permanent failure (like a webhook being deleted - but more on that later).&lt;/p&gt;

&lt;p&gt;Because it can happen that a flush of the queue and a sweep happen at the same time, I added another state to a log event: &lt;code&gt;sending&lt;/code&gt;&lt;br&gt;&lt;br&gt;
This is used mainly for the sweep: The sweep will retry events with a state of &lt;code&gt;pending&lt;/code&gt; and older than 30 seconds - or &lt;code&gt;sending&lt;/code&gt; with a last updated timestamp older than 2 minutes (which indicates a crash mid-flush). Those get re-queued for sending.&lt;/p&gt;

&lt;p&gt;The whole delivery state machine looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pending -&amp;gt; sending -&amp;gt; sent / skipped / dead
sending -&amp;gt; pending (transient error, under attempts cap -&amp;gt; retried by sweep)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attempts cap (10) only applies to transient send errors — once a batch hits it, event goes &lt;code&gt;dead&lt;/code&gt; instead of back to &lt;code&gt;pending&lt;/code&gt;. &lt;code&gt;skipped&lt;/code&gt; doesn't touch attempts at all (webhook creation failure, thread gone, dead-webhook cooldown not elapsed).&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-healing Webhooks on Demand
&lt;/h2&gt;

&lt;p&gt;On demand webhooks are something I've seen in the Sapphire bot and I really liked it. Instead of creating a webhook every time a config is saved, the bot will only create a webhook when it is needed - if there isn't a webhook for the channel yet. This also includes threads.&lt;br&gt;&lt;br&gt;
The webhook token is also encrypted at rest, so that it can't be read by anyone who has access to the database. This uses the same system that is also used for user access tokens.&lt;/p&gt;

&lt;p&gt;While testing, I came across what I had confidently ignored until now: the edge case error handling for webhooks. What happens if a webhook is deleted? Or if the bot doesn't have permission to create a webhook in the channel? Or if the webhook is deleted and the bot doesn't have permissions to create a new one - and then optionally later gets the permission again?&lt;/p&gt;

&lt;p&gt;This might seem like very nitpicky edge cases, but they are very important to handle properly.&lt;br&gt;&lt;br&gt;
There are a few questions that need to be answered here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What happens to the log events that are in the queue for this webhook?&lt;/li&gt;
&lt;li&gt;What happens to the log events that are created while the webhook is down?&lt;/li&gt;
&lt;li&gt;What do we try to fix the webhook? How often? For how long? What happens if we can't fix it?&lt;/li&gt;
&lt;li&gt;How do we let this be known to the server admins/mods?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Until now, the bot would just mark the webhook as &lt;code&gt;dead&lt;/code&gt; and stop trying to send log events to it. This is not a good solution, as it would lead to support tickets and confusion. The bot should try to fix the webhook and if it can't, it should let the server admins/mods know that there is an issue with the webhook.&lt;br&gt;&lt;br&gt;
After some thinking with the help of Claude, I came up with a solution that would handle all of these questions.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;DBWebhook&lt;/code&gt; model now gets an additional state, &lt;code&gt;recreating&lt;/code&gt;, on top of &lt;code&gt;active&lt;/code&gt; and &lt;code&gt;dead&lt;/code&gt;. The transition from &lt;code&gt;active&lt;/code&gt; to &lt;code&gt;recreating&lt;/code&gt; is claimed atomically - a &lt;code&gt;findOneAndUpdate&lt;/code&gt; that filters on both &lt;code&gt;_id&lt;/code&gt; and the expected current &lt;code&gt;state&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Worth noting: a channel's buffer isn't locked while a flush is in flight. The batch itself is safe - &lt;code&gt;flush()&lt;/code&gt; swaps &lt;code&gt;buffer.events&lt;/code&gt; out synchronously before doing anything async, so one call can never process the same batch twice. But nothing stops a &lt;em&gt;second&lt;/em&gt; &lt;code&gt;flush()&lt;/code&gt; call for the same channel from starting while the first is still awaiting Discord - the early-flush-at-10 path and the 5s timer path can both fire close together, and the sweep re-enqueuing stale events can kick off another one too. So two flushes for the same channel, each with their own batch, can genuinely hit a terminal Discord error at roughly the same time. That's exactly the scenario the atomic claim guards against: only one of them wins the claim; the other gets &lt;code&gt;null&lt;/code&gt; back and backs off, leaving its batch as &lt;code&gt;pending&lt;/code&gt; for the sweep to pick up later. Without that guard, both flushes would try to recreate the same webhook and I'd end up with either duplicate webhooks or a race writing stale tokens over fresh ones.&lt;/p&gt;

&lt;p&gt;Only &lt;em&gt;terminal&lt;/em&gt; Discord errors trigger a recreate attempt in the first place - unknown channel/webhook or missing permissions. Anything else (a 500, a timeout, a random ratelimit hiccup) is treated as transient and just bumps &lt;code&gt;attempts&lt;/code&gt; like normal; there's no reason to nuke and rebuild a webhook that's probably fine.&lt;/p&gt;

&lt;p&gt;Once a flush claims the recreate, it tries to create a fresh webhook for the channel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Success&lt;/strong&gt; → &lt;code&gt;recreating → active&lt;/code&gt;, new &lt;code&gt;webhookId&lt;/code&gt;/token saved, and the flush retries the held send once, inline, with the new webhook client. No need to wait for the next batch window - the events that triggered the recreate get delivered immediately if the retry succeeds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure&lt;/strong&gt; → &lt;code&gt;recreating → dead&lt;/code&gt;, with a &lt;code&gt;deadReason&lt;/code&gt; (&lt;code&gt;missing_permissions&lt;/code&gt; or &lt;code&gt;unknown_channel&lt;/code&gt;) persisted so the dashboard can show &lt;em&gt;why&lt;/em&gt;, not just &lt;em&gt;that&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's also a stale-claim fallback: if a row sits in &lt;code&gt;recreating&lt;/code&gt; for more than 30 seconds, the next flush that hits it treats the claim as abandoned (crashed process, most likely) and re-claims it rather than waiting forever on a recreate that's never coming.&lt;/p&gt;

&lt;p&gt;For events that show up while a webhook is &lt;code&gt;dead&lt;/code&gt;, I didn't want to just silently drop them and I didn't want to spam a full retry either - Discord permissions/channel issues don't usually get fixed instantly. So &lt;code&gt;dead&lt;/code&gt; webhooks self-heal on a 15-minute cooldown: the first flush attempt after the cooldown elapses gets to re-claim and try again, same atomic guard as above. Until then, batches for that channel are marked &lt;code&gt;skipped&lt;/code&gt; (not &lt;code&gt;dead&lt;/code&gt;, not endlessly retried) - &lt;code&gt;attempts&lt;/code&gt; isn't touched at all, since the problem isn't the event, it's the webhook.&lt;/p&gt;

&lt;p&gt;The one thing I did want to be loud about is the actual &lt;code&gt;recreating → dead&lt;/code&gt; transition - that's the point where a human needs to know something's wrong. It fires exactly once per transition (not once per skipped batch after that): a DM to the guild owner, plus a plain non-webhook message dropped into the affected channel (directly by the bot) if the bot can still post there, both carrying the reason. The dashboard also gets &lt;code&gt;webhookState&lt;/code&gt;/&lt;code&gt;deadReason&lt;/code&gt; surfaced, so a warning dialog can show up there too - it displays a dialog with the things that need attention and stores it in the browser's session storage.&lt;/p&gt;

&lt;p&gt;The state machine end to end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;active -&amp;gt; recreating (terminal error, claim succeeds)
recreating -&amp;gt; active (recreate succeeds, held send retried inline)
recreating -&amp;gt; dead (recreate fails)
dead -&amp;gt; recreating (cooldown elapsed [15min], claim succeeds)
recreating -&amp;gt; recreating (claim stale [30s], re-claimed)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Diffing changes without lying about what changed
&lt;/h2&gt;

&lt;p&gt;Once events are actually getting delivered, the next problem is: what do you put in them? "Someone updated the category" isn't useful. "Someone changed the staff role from @Support to @Moderators" is.&lt;/p&gt;

&lt;p&gt;My first instinct was the obvious one - diff the whole before/after Mongoose document and see what's different. That lasted about five minutes before I threw it out. Mongoose documents carry &lt;code&gt;updatedAt&lt;/code&gt;, internal &lt;code&gt;__v&lt;/code&gt; versioning, and whatever else Mongoose likes to touch on save - none of which is a real change a human made. Diffing the whole document means every save produces phantom entries, and phantom entries are worse than no logging at all, because now nobody trusts the log.&lt;/p&gt;

&lt;p&gt;So instead: never diff a whole document. Diff an explicit whitelist of fields per model.&lt;/p&gt;

&lt;p&gt;The first version of that whitelist was just a flat &lt;code&gt;string[]&lt;/code&gt; of field names. That held up for maybe two models before it broke. Some fields carry secrets (a message's &lt;code&gt;password&lt;/code&gt; or a guild's &lt;code&gt;encryptedApiKey&lt;/code&gt;) - I want the log to say "password was changed" without ever writing the actual password anywhere near Mongo or Discord. Some fields are big nested objects (&lt;code&gt;Form.fields&lt;/code&gt;, a whole component tree) - diffing them is fine, but dumping the raw before/after into a Discord embed is both useless (nobody reads a JSON blob in a Discord message) and eventually just breaks the text length limit. And some fields are flat-ish sub-objects (Shield's &lt;code&gt;categories&lt;/code&gt;) where I want a per-subkey diff, not "the whole &lt;code&gt;categories&lt;/code&gt; object changed."&lt;/p&gt;

&lt;p&gt;That's what pushed me toward a small tagged-union type instead of a flat list of strings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FieldSpec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;opaque&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;redacted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;nested&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FieldSpec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;A plain key is the default case - whole-value diff, store both sides as-is.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;redacted&lt;/code&gt; still runs through the same deep-equal check, so a password rotation &lt;em&gt;is&lt;/em&gt; detected and &lt;em&gt;does&lt;/em&gt; show up as a change - the value just never survives past the diff itself. It's replaced with &lt;code&gt;"[redacted]"&lt;/code&gt; before the change is even recorded, so there's no later "ope, don't render the raw value" step to forget - the raw value simply never reaches Mongo and the embed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;opaque&lt;/code&gt; is diffed and stored in full (the dashboard wants the real structure to show a proper diff view), but flows through a different code path at render time - more on that below.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nested&lt;/code&gt; recurses into a sub-object and diffs only the named sub-keys, dot-joining the path as it goes (&lt;code&gt;categories.ipAddresses: false → true&lt;/code&gt;), so nested entries can themselves be &lt;code&gt;redacted&lt;/code&gt; or nested again, arbitrarily deep.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The actual comparison is &lt;code&gt;Bun.deepEquals(a, b, true)&lt;/code&gt; - strict mode, no new dependency, the same engine that backs &lt;code&gt;bun:test&lt;/code&gt;'s &lt;code&gt;toEqual&lt;/code&gt;. The web dashboard can't use Bun-only APIs, so it ports the identical &lt;code&gt;FieldSpec&lt;/code&gt; logic on top of &lt;code&gt;@the-lukez/fast-deep-equal&lt;/code&gt; instead - same contract, two engines, so a change diffed on the dashboard side looks exactly like one diffed on the bot side by the time it reaches the database. If there are any conflicts in the future, I will just make a small shared package for the diffing logic.&lt;/p&gt;

&lt;p&gt;One more issue worth mentioning: some dashboard endpoints save a whole collection in one request - create, update, delete, and untouched rows, all mixed together in a single payload. Diffing that naively against "what's in the request" isn't enough, because "untouched" rows are still sitting right there in the payload looking like updates. So those endpoints reconcile against the DB first: load what currently exists, diff each incoming row against its existing counterpart by id, and drop anything where nothing actually changed. One log event per entity that genuinely changed, zero events for a no-op save.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rendering: from a diff to a readable Discord embed
&lt;/h2&gt;

&lt;p&gt;Diffing gets you a list of &lt;code&gt;{ key, oldValue, newValue }&lt;/code&gt; triples. Turning that into something a server admin actually wants to read is its own problem, and it's got one hard rule underneath it: no raw ID is ever shown bare. A Discord snowflake sitting alone in an embed (&lt;code&gt;728...492&lt;/code&gt;) tells nobody anything - it has to become a mention. A Mongo ObjectId is worse, since it's not even resolvable by the reader at all.&lt;/p&gt;

&lt;p&gt;Snowflakes are easy - wrap in &lt;code&gt;&amp;lt;@id&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;#id&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;@&amp;amp;id&amp;gt;&lt;/code&gt; and Discord does the rest client-side. ObjectIds need an actual DB lookup to become a name (which category, which form, which message), and I didn't want a round trip per changed field. So &lt;code&gt;toEmbed()&lt;/code&gt; walks the whole change-set for a doc up front, collects every referenced category/form/message id into a &lt;code&gt;Set&lt;/code&gt;, and resolves each model in exactly one batched query - &lt;code&gt;find all ticket categories of which the id matches one of these ids:&lt;/code&gt; and so on - before rendering a single line. The embed itself renders synchronously off those resolved maps afterward.&lt;br&gt;&lt;br&gt;
The dashboard even provides a metadata field entry with a &lt;code&gt;targetValue&lt;/code&gt; which carries the content to use for a given snowflake or ObjectId on almost every log event.&lt;/p&gt;

&lt;p&gt;For everything else there's a small per-field config table instead of scattered ad hoc formatting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RenderKind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;plain&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bool&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;channel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;userList&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;roleList&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;channelList&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;category&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;categoryList&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;opaque&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;redacted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;countOnly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each changed key for a given log group maps to a label + a &lt;code&gt;RenderKind&lt;/code&gt;, and the renderer just looks it up - &lt;code&gt;staffRoles&lt;/code&gt; is a &lt;code&gt;roleList&lt;/code&gt;, &lt;code&gt;claimedBy&lt;/code&gt; is a &lt;code&gt;user&lt;/code&gt;, a Shield custom-pattern list is &lt;code&gt;countOnly&lt;/code&gt; (nobody needs to see the raw regexes in a Discord embed, just "3 → 5"). &lt;code&gt;opaque&lt;/code&gt; fields - the ones diffed and stored in full for the dashboard - get suppressed here specifically: the embed shows "&lt;em&gt;Changed&lt;/em&gt; — &lt;code&gt;[View in dashboard](...)&lt;/code&gt;" instead of trying to cram a component tree into a message. &lt;code&gt;redacted&lt;/code&gt; fields render as a fixed "[redacted]" string, same as they were stored.&lt;/p&gt;

&lt;p&gt;A handful of fields don't fit a flat before/after line at all, so they get dedicated nested renderers instead of forcing them through the generic table: permission overrides diff as a list of ➕ added / ✏️ changed / ➖ removed lines per role or user, allowed-mentions gets summarized into one line ("parse: everyone | 2 roles, 1 user") instead of dumping the raw object, and authorized bots render as one line per bot with who added it and to which channel. These are maybe 5% of all fields by count, but they're the ones that would look like garbage if forced through the generic &lt;code&gt;oldValue → newValue&lt;/code&gt; renderer ("123" → "456").&lt;br&gt;&lt;br&gt;
Fun fact: I basically copied Discord's Permission system with the bitfields to save space in the DB.&lt;/p&gt;

&lt;p&gt;Since Ticketon is localized, I must also resolve the locale once per delivery batch, not once per event - the dispatcher already groups everything going out in one flush by guild, so it resolves the guild's locale once and sets it on an ambient &lt;code&gt;AsyncLocalStorage&lt;/code&gt; context (&lt;code&gt;withLocale&lt;/code&gt;) that every &lt;code&gt;m.*()&lt;/code&gt; call inside &lt;code&gt;toEmbed()&lt;/code&gt; reads from. Rendering forty events in a burst doesn't mean forty locale lookups.&lt;br&gt;&lt;br&gt;
This reuses the exising &lt;a href="https://li18n.thelukez.com/" rel="noopener noreferrer"&gt;li18n&lt;/a&gt; setup I built for localizing the bot. Since I added it, manual locale juggling is no longer needed anywhere when doing functional programming - seriously, I'm really proud of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's still open
&lt;/h2&gt;

&lt;p&gt;I'd be lying if I said this was finished. A few things are known trade-offs that I decided to live with for now rather than swept-under-the-rug bugs - and I'd rather name them than pretend the system is perfect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No batch/request grouping in &lt;code&gt;LogEvent&lt;/code&gt;.&lt;/strong&gt; Right now every changed entity becomes its own &lt;code&gt;LogEvent&lt;/code&gt; row. If someone saves a screen on the dashboard that touches 5 messages in one request, that's 5 separate rows in the audit-log list UI. On the Discord side this doesn't really matter - the dispatcher's per-channel batching already collapses them into a single message with up to 10 embeds, so a human reading the channel sees one clean batch. But in the dashboard list, those 5 rows aren't grouped under "one save," and there's no &lt;code&gt;requestId&lt;/code&gt;/&lt;code&gt;batchId&lt;/code&gt; concept tying them together like there was before. It's not wrong, it's just not as tidy as it could be. Adding a grouping id later is a purely additive change, so I left it out for now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single-process assumption in the dispatcher.&lt;/strong&gt; The in-process buffer, the 5s window, the sweep - all of it assumes exactly one process owns the dispatcher. That's true today: the bot runs as a single process. The moment I add sharding, two processes could both hold a buffer for the same channel and both run the sweep, which reopens all the double-post races the &lt;code&gt;sending&lt;/code&gt; lease was built to close. The known answer is to pin the dispatcher to shard 0, or move the sweep behind a Mongo lock so only one process ever runs it. or I just completely extract it and run it separately with the API - communication between it and the shards would then be done via IPC. I didn't build any of these, because building for a shard count I don't have yet is exactly the Cloudflare-Worker mistake from the start of this post, just wearing a different hat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retention.&lt;/strong&gt; No bespoke logging cleanup job. Log events get a 30-day TTL index in Mongo, so old rows expire on their own, and guild-delete purging is folded into the existing &lt;code&gt;deleteData()&lt;/code&gt; sweep that already handles GDPR erasure for everything else. One cleanup path for the whole system, not a special case for logs. This was the one of the main points of killing the D1 database - retention stops being a two-system problem the second there's only one database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;The thing I keep coming back to is how much better the boring answer turned out to be. The first design had a Cloudflare Worker, a D1 database, a Queue, and three signed legs of auth - and it was, on paper, the "proper" distributed-systems way to build this. It lost. Not to a cleverer distributed design, but to ~7 files sitting inside the bot I already had, reusing the signed &lt;code&gt;/dashboard&lt;/code&gt; endpoint I'd already built, writing to the database I already had.&lt;/p&gt;

&lt;p&gt;It lost the moment I took a step back and assessed the goal again: I needed a &lt;strong&gt;single writer and a single schema&lt;/strong&gt;, not massive scale. The Worker was solving a scaling problem I don't have at the time (a handful of events a minute) while quietly creating a retention problem, an auth problem, and a "how does the edge get the webhook config" problem that I did have. Once the real goal had a name, the architecture more or less picked itself.&lt;/p&gt;

&lt;p&gt;So if there's one thing to take from this: before you reach for the impressive architecture, say the actual requirement out loud/write it down and check that it's the one you're building for. Sometimes it is. Mine wasn't.&lt;/p&gt;

</description>
      <category>discord</category>
      <category>architecture</category>
      <category>mongodb</category>
      <category>backend</category>
    </item>
    <item>
      <title>How to Localize your Discord Bot in 2026</title>
      <dc:creator>LukeZ</dc:creator>
      <pubDate>Thu, 02 Apr 2026 08:51:00 +0000</pubDate>
      <link>https://dev.to/thelukez/how-to-localize-your-discord-bot-in-2026-3o5g</link>
      <guid>https://dev.to/thelukez/how-to-localize-your-discord-bot-in-2026-3o5g</guid>
      <description>&lt;p&gt;Discord bots reach users across the globe. If your bot replies in English-only (or whatever language you choose), you're leaving a huge portion of your audience with a worse experience.&lt;br&gt;
Discord even provides a &lt;code&gt;locale&lt;/code&gt; field on every interaction, so you already know what language each user prefers - you just need to use it.&lt;/p&gt;

&lt;p&gt;This guide walks through building a localized Discord bot using &lt;strong&gt;li18n&lt;/strong&gt;, a compile-time i18n library for TypeScript that turns your JSON message files into fully type-safe functions with zero runtime overhead.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I've already wrote a guide on this some months ago, but that was with a different package - which is not designed for such stuff and also had some bugs.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Why li18n?
&lt;/h2&gt;

&lt;p&gt;Most i18n libraries follow the same pattern: load a big JSON file at runtime, look up a string by key, interpolate variables. This works, but it has downsides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No type safety - mistyped keys and missing variables are silent bugs&lt;/li&gt;
&lt;li&gt;Runtime parsing cost on every message lookup&lt;/li&gt;
&lt;li&gt;Locale switching requires extra plumbing per request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;li18n takes a different approach. You define your messages in JSON, run &lt;code&gt;li18n build&lt;/code&gt;, and get plain TypeScript functions - one per message key.&lt;br&gt;
The generated code is just functions and &lt;code&gt;switch&lt;/code&gt; statements. There's nothing to parse at runtime, and your editor knows exactly which parameters each message requires.&lt;/p&gt;
&lt;h2&gt;
  
  
  Installing
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun add @the-lukez/li18n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The CLI is &lt;code&gt;li18n&lt;/code&gt;. Run &lt;code&gt;li18n init&lt;/code&gt; to scaffold your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun li18n init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a &lt;code&gt;li18n.config.json&lt;/code&gt; and a &lt;code&gt;messages/&lt;/code&gt; directory with example locale files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project layout
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-bot/
├── messages/
│   ├── en.json
│   └── de.json
├── src/
│   ├── i18n/          ← generated, don't edit
│   ├── commands/
│   └── index.ts
├── li18n.config.json
└── index.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;li18n.config.json&lt;/code&gt; tells the compiler where your messages live and where to write the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"locales"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"de"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"defaultLocale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"messagesDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./messages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outputDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src/i18n"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Defining messages
&lt;/h2&gt;

&lt;p&gt;Messages live in &lt;code&gt;messages/&amp;lt;locale&amp;gt;.json&lt;/code&gt;. Every locale file must have the same keys - li18n will tell you if anything is missing or mismatched when you run &lt;code&gt;li18n check&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;messages/en.json (expand)&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ping"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reply"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pong! Latency: {latency}ms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Could not measure latency."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"assigned"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"You've been given the {role} role."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"removed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The {role} role has been removed from you."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"adminOnly"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This command is only available to admins."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"remind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"set"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Got it! I'll remind you in {minutes} minutes."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fire"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hey {username}, here's your reminder:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;gt; {message}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tooShort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"var"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"num"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"minutes"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"cases"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"=== 0"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"You need to give me at least 1 minute."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"else"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"That's too short - minimum is 1 minute."&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"online"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bot is online."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Running version {version}."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"shards"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"var"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"num"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"count"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"cases"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"=== 1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1 shard connected."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"else"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{count} shards connected."&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;messages/de.json (expand)&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ping"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reply"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pong! Latenz: {latency}ms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Latenz konnte nicht gemessen werden."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"assigned"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Du hast die Rolle {role} erhalten."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"removed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Die Rolle {role} wurde dir entfernt."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"adminOnly"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dieser Befehl ist nur für Admins verfügbar."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"remind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"set"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Verstanden! Ich erinnere dich in {minutes} Minuten."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fire"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hey {username}, hier ist deine Erinnerung:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;gt; {message}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tooShort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"var"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"num"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"minutes"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"cases"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"=== 0"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Du brauchst mindestens 1 Minute."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"else"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Das ist zu kurz - Minimum ist 1 Minute."&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"online"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bot ist Online."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Version: {version}."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"shards"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"var"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"num"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"count"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"cases"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"=== 1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1 Shards verbunden."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"else"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{count} Shards verbunden."&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;A few things to notice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nested keys&lt;/strong&gt; - messages can be nested arbitrarily deep. The compiler flattens them into dot-separated keys (&lt;code&gt;ping.reply&lt;/code&gt;, &lt;code&gt;role.assigned&lt;/code&gt;, etc).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variables&lt;/strong&gt; - &lt;code&gt;{latency}&lt;/code&gt; becomes a required typed parameter &lt;code&gt;latency: string&lt;/code&gt; (or &lt;code&gt;number&lt;/code&gt;/&lt;code&gt;boolean&lt;/code&gt; when you use the typed syntax).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditionals&lt;/strong&gt; - the &lt;code&gt;remind.tooShort&lt;/code&gt; and &lt;code&gt;status.shards&lt;/code&gt; keys use numeric conditionals. The compiler turns these into ternary expressions, not runtime lookups.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See the &lt;a href="https://dev.to/guide/message-format"&gt;message format reference&lt;/a&gt; for more details on the syntax and features available in message files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compiling
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun li18n build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;src/i18n/&lt;/code&gt; directory is now populated with generated TypeScript. You can also run &lt;code&gt;li18n watch&lt;/code&gt; during development to recompile on every save.&lt;/p&gt;

&lt;p&gt;To validate that all locales are consistent without writing any files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun li18n check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The generated API
&lt;/h2&gt;

&lt;p&gt;After compilation, your entry point is &lt;code&gt;src/i18n/index.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./messages/_index.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;overwriteGetLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;withLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseLocale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./runtime.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MaybePromise&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./runtime.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Import &lt;code&gt;m&lt;/code&gt; for messages and &lt;code&gt;withLocale&lt;/code&gt; for locale-scoped handlers. Every message function accepts an optional explicit locale as its last argument - but when you use &lt;code&gt;withLocale&lt;/code&gt;, you rarely need to pass it manually.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;withLocale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./src/index.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pingReply&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;latency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// uses current locale&lt;/span&gt;
&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pingReply&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;latency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;de&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// explicit override&lt;/span&gt;
&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;statusShards&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// number conditional, no locale arg needed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TypeScript will catch you if you forget a required parameter or pass the wrong type.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into Discord.js
&lt;/h2&gt;

&lt;p&gt;Here's where li18n really shines. Discord sends a &lt;code&gt;locale&lt;/code&gt; string with every interaction (e.g. &lt;code&gt;"en-US"&lt;/code&gt;, &lt;code&gt;"de"&lt;/code&gt;, &lt;code&gt;"fr"&lt;/code&gt;). We want each interaction handler to automatically use the right locale without passing it around everywhere.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;withLocale&lt;/code&gt; wraps any async function and stores the locale in &lt;code&gt;AsyncLocalStorage&lt;/code&gt;. Every &lt;code&gt;m.*&lt;/code&gt; call inside the handler reads it automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resolving the locale
&lt;/h3&gt;

&lt;p&gt;Discord uses IETF language tags like &lt;code&gt;en-US&lt;/code&gt;, but your locales are likely &lt;code&gt;en&lt;/code&gt;, &lt;code&gt;de&lt;/code&gt;, etc. - So you need to write a small resolver:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Locale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./i18n/index.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Interaction&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;discord.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveLocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Interaction&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Locale&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// e.g. "en-US", "de", "pt-BR"&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Locale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locales&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ping Command&lt;/p&gt;

&lt;p&gt;&lt;code&gt;src/commands/ping.ts&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SlashCommandBuilder&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;discord.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ChatInputCommandInteraction&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;discord.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;withLocale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../i18n/index.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resolveLocale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../i18n/locale.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SlashCommandBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ping&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDescription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Check the bot latency&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;execute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withLocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChatInputCommandInteraction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Pinging…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fetchReply&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;latency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdTimestamp&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdTimestamp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;editReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pingReply&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;latency&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;resolveLocale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execute&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;No locale argument anywhere in the handler body - &lt;code&gt;m.pingReply&lt;/code&gt; calls &lt;code&gt;getLocale()&lt;/code&gt; internally and gets the right value from the &lt;code&gt;AsyncLocalStorage&lt;/code&gt; context that &lt;code&gt;withLocale&lt;/code&gt; set up.&lt;/p&gt;

&lt;p&gt;Role Command&lt;/p&gt;

&lt;p&gt;&lt;code&gt;src/commands/role.ts&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SlashCommandBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PermissionFlagsBits&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;discord.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ChatInputCommandInteraction&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;discord.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;withLocale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../i18n/index.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resolveLocale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../i18n/locale.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SlashCommandBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDescription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Toggle a role&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addRoleOption&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setDescription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The role to toggle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setRequired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;execute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withLocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChatInputCommandInteraction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;memberPermissions&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PermissionFlagsBits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ManageRoles&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;roleAdminOnly&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;ephemeral&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;member&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;guild&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasRole&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;roleRemoved&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;roleAssigned&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;resolveLocale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execute&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Reminder Command&lt;/p&gt;

&lt;p&gt;&lt;code&gt;src/commands/remind.ts&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SlashCommandBuilder&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;discord.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ChatInputCommandInteraction&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;discord.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;withLocale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../i18n/index.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resolveLocale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../i18n/locale.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SlashCommandBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;remind&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDescription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Set a reminder&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addIntegerOption&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;minutes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setDescription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Minutes from now&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setRequired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addStringOption&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setDescription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;What to remind you of&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setRequired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;execute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withLocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChatInputCommandInteraction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;minutes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;minutes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;minutes&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The conditional picks the right case based on the value of `minutes`&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remindTooShort&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;minutes&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="na"&gt;ephemeral&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remindSet&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;minutes&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;followUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remindFire&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;minutes&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;resolveLocale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execute&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;code&gt;src/index.ts&lt;/code&gt; - the main bot file&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;GatewayIntentBits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Collection&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;discord.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ChatInputCommandInteraction&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;discord.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ping&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./src/commands/ping.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./src/commands/role.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;remind&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./src/commands/remind.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;intents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;GatewayIntentBits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Guilds&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commands&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Collection&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ping&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ping&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;remind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remind&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;interactionCreate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isChatInputCommand&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// We only got slash commands in this example, so ignore other interactions&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commandName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ChatInputCommandInteraction&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// type inference on withLocale ensures the function has the same params as the handler&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DISCORD_TOKEN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you got a lot of commands with the same locale resolver, you can create a helper class that wraps &lt;code&gt;withLocale&lt;/code&gt; and hardcodes the resolver, so you don't have to repeat it everywhere.&lt;/p&gt;

&lt;p&gt;Example interaction handler class&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;withLocale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./src/i18n/index.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resolveLocale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./src/locale.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ClientEvents&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;discord.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InteractionHandler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Collection&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;components&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Collection&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Collection&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;,&lt;/span&gt;
    &lt;span class="nx"&gt;components&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Collection&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;components&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;components&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ClientEvents&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;interactionCreate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;withLocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isChatInputCommand&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commandName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isButton&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;resolveLocale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// All interactions go through the same locale resolver&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// In your index.ts&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InteractionHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;components&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;interactionCreate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;In your command handlers, you can now use &lt;code&gt;m.*&lt;/code&gt; without worrying about &lt;code&gt;withLocale&lt;/code&gt; or the resolver - it's all handled by the &lt;code&gt;InteractionHandler&lt;/code&gt; class.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ChatInputCommandInteraction&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;discord.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../i18n/index.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChatInputCommandInteraction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pingReply&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;latency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  How concurrent requests stay isolated
&lt;/h2&gt;

&lt;p&gt;Because &lt;code&gt;withLocale&lt;/code&gt; uses &lt;code&gt;AsyncLocalStorage&lt;/code&gt; under the hood, two simultaneous interactions never see each other's locale. If a German user and an English user both trigger &lt;code&gt;/ping&lt;/code&gt; at the same moment, each &lt;code&gt;withLocale&lt;/code&gt; call creates its own isolated storage slot. There's no global state to race on.&lt;/p&gt;

&lt;p&gt;This also means you can override the locale mid-handler if needed. Say you want to log something in English regardless of the user's locale:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;overwriteGetLocale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../i18n/index.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;execute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withLocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Responds in the user's language&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pingReply&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;latency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="c1"&gt;// Switch to English just for this log message&lt;/span&gt;
  &lt;span class="nf"&gt;overwriteGetLocale&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pingReply&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;latency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt; &lt;span class="c1"&gt;// always in English&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;resolveLocale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;overwriteGetLocale&lt;/code&gt; only affects the current &lt;code&gt;withLocale&lt;/code&gt; context - other concurrent handlers are unaffected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Development workflow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Watch for changes and recompile as you edit message files&lt;/span&gt;
bunx li18n watch

&lt;span class="c"&gt;# Verify all locales are in sync before committing&lt;/span&gt;
bunx li18n check

&lt;span class="c"&gt;# One-shot compile for CI/CD&lt;/span&gt;
bunx li18n build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since the output directory is auto-generated, add it to &lt;code&gt;.gitignore&lt;/code&gt; (li18n does this automatically) and rebuild as part of your deployment step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With watch mode, it is recommended to change your save behavior in your editor to "save on focus change" or increase the debounce delay, to avoid triggering a build on every keystroke (this could result in bugs).&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full type safety&lt;/strong&gt; - every message function's parameters are inferred from the JSON. Forget &lt;code&gt;{ latency }&lt;/code&gt; and TypeScript tells you before you ship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero runtime parsing&lt;/strong&gt; - compiled messages are plain functions. No JSON loaded, no key lookups, no format-string parsing per call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-interaction locale isolation&lt;/strong&gt; - &lt;code&gt;withLocale&lt;/code&gt; handles the plumbing via &lt;code&gt;AsyncLocalStorage&lt;/code&gt;. No locale prop-drilling. This should even work in serverless environments where multiple requests share the same instance - however you might need to enable &lt;code&gt;nodejs_compat&lt;/code&gt; for example, if you're on Cloudflare Workers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Numeric and boolean conditionals&lt;/strong&gt; - pluralization and conditional text compile to ternary chains, not runtime switch tables.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>discord</category>
      <category>discordbot</category>
      <category>i18n</category>
    </item>
    <item>
      <title>How to make a localized Discord Bot with Paraglide.js</title>
      <dc:creator>LukeZ</dc:creator>
      <pubDate>Wed, 29 Oct 2025 09:23:15 +0000</pubDate>
      <link>https://dev.to/thelukez/how-to-make-a-localized-discord-bot-with-paraglidejs-1eb8</link>
      <guid>https://dev.to/thelukez/how-to-make-a-localized-discord-bot-with-paraglidejs-1eb8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Banner by &lt;a href="https://unsplash.com/@leookubo" rel="noopener noreferrer"&gt;Leonardo Toshiro Okubo&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/unknown-person-writing-on-chalkboard-jBSTNenQxok" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you want to make a Discord bot that speaks multiple languages, the question is inevitable: &lt;strong&gt;"HOW?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I’ve done it the hard way - my own JSON loader, manual locale passing, &lt;code&gt;require("./en.json")&lt;/code&gt;, string lookups everywhere. It worked. Barely.&lt;/p&gt;

&lt;p&gt;But what if you didn’t have to manage the locale yourself &lt;em&gt;every single time&lt;/em&gt; you needed a translation?&lt;/p&gt;

&lt;p&gt;You write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;greeting&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Alex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it just works - in the user’s language, with correct grammar, plurals, and more.&lt;/p&gt;

&lt;p&gt;Let’s set it up!&lt;/p&gt;

&lt;h2&gt;
  
  
  Paraglide JS
&lt;/h2&gt;

&lt;p&gt;Paraglide is a lightweight, type-safe internationalization (i18n) library for JavaScript and TypeScript projects, designed to make multi-language support effortless and scalable. Built by the inlang team, it eliminates the boilerplate of traditional translation systems by generating optimized code from your translation files at build time. No more manual locale management, runtime JSON parsing, or error-prone string interpolation—Paraglide handles it all with zero runtime overhead.&lt;/p&gt;

&lt;p&gt;Unlike my old-school JSON-based translation handler (which basically boiled down to &lt;code&gt;require("path/to/translations/en.json")&lt;/code&gt; and manual locale juggling every time), Paraglide lets you focus on your bot's logic. You define translations once in simple .json files, run a quick build step, and get fully typed functions for every message. Want to fetch a greeting in French? Just call &lt;code&gt;m.greeting({ user: 'Pierre' })&lt;/code&gt; - it (kind of) auto-detects the locale from your Discord user's settings and returns the translated string. Types ensure no typos or missing params, catching errors at compile time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inlang Message Format
&lt;/h3&gt;

&lt;p&gt;Inlang's message format even provides a very nice solution for built-in variants, pluralization and more!&lt;/p&gt;

&lt;p&gt;See their &lt;a href="https://inlang.com/m/reootnfj/plugin-inlang-messageFormat#complex-messages-with-variants-pluralization-etc" rel="noopener noreferrer"&gt;docs&lt;/a&gt; for more info.&lt;/p&gt;

&lt;p&gt;Using it, I can easily do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;greetUsers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;declarations&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input count&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;local countPlural = count: plural&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;selectors&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;countPlural&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;match&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;countPlural=one&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Say hello to {count} user!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;countPlural=other&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Say hello to {count} users!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Paraglide Setup
&lt;/h2&gt;

&lt;p&gt;Before we can really start, we need to initialize paraglide.&lt;/p&gt;

&lt;p&gt;In my example I have the following project structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
└── src/
    ├── commands/
    │   └── someCommand.ts
    ├── components/
    │   └── someComponent.ts
    ├── events/
    │   └── messageCreate/
    │       └── index.ts
    └── index.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have my own solution for loading all commands, components and events, but that isn't important here. More on that later.&lt;/p&gt;

&lt;p&gt;Now we need to run the init wizard.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# npm&lt;/span&gt;
npx @inlang/paraglide-js@latest init
&lt;span class="c"&gt;# pnpm&lt;/span&gt;
pnpx @inlang/paraglide-js@latest init
&lt;span class="c"&gt;# bun (I use bun in my project, but that doesn't matter)&lt;/span&gt;
bunx @inlang/paraglide-js@latest init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It will ask you some stuff and you should do the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where should the compiled files be placed? &lt;code&gt;./src/paraglide&lt;/code&gt; (default)&lt;/li&gt;
&lt;li&gt;Do you want to set up machine translations? If you want, yes. It will add a script in your package.json for that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This will create&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;project/messages&lt;/code&gt; for translations

&lt;ul&gt;
&lt;li&gt;Locale files for messages look like &lt;code&gt;en.json&lt;/code&gt; or &lt;code&gt;de-DE.json&lt;/code&gt; in this directory.&lt;/li&gt;
&lt;li&gt;This has the message &lt;code&gt;example_message&lt;/code&gt; in it by default&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;project/project.inlang&lt;/code&gt; for settings&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;project/src/paraglide/&lt;/code&gt; the directory for compiled translations ("messages")&lt;/li&gt;
&lt;li&gt;A script to compile translations for runtime called &lt;code&gt;build&lt;/code&gt; (you can change that if you want - I changed it to &lt;code&gt;compile-translations&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A script to run machine translations: &lt;code&gt;machine-translate&lt;/code&gt;
More on that later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;DON'T FORGET TO RUN &lt;code&gt;npm i&lt;/code&gt; AFTER INIT!&lt;/strong&gt; (or whatever your package manager is)&lt;/p&gt;

&lt;h2&gt;
  
  
  Discord.js Bot Setup
&lt;/h2&gt;

&lt;p&gt;There is no big difference for handling interactions and events, however we need to add a sort of middleware.&lt;/p&gt;

&lt;p&gt;Paraglide can't detect the locale just by itself. But it supports &lt;code&gt;AsyncLocalStorage&lt;/code&gt; which we will use here. &lt;code&gt;AsyncLocalStorage&lt;/code&gt; enables you to store data (like primitives or objects) that remains accessible throughout an asynchronous call stack, automatically propagating the context across &lt;code&gt;await&lt;/code&gt;s and callbacks without manual passing. It’s ideal for tracking per-request state, such as locales, in async-heavy environments like Discord bots.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// project/src/index.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ClientEvents&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;discord.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;baseLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;overwriteGetLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Locale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./paraglide/runtime.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;localeStorage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;AsyncLocalStorage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Locale&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;locales&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// We need to determine the locale when calling the "get message" function in an interaction&lt;/span&gt;
&lt;span class="nf"&gt;overwriteGetLocale&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;localeStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;baseLocale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="c1"&gt;// Wrapper for an interaction handler&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;interactionMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ClientEvents&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;interactionCreate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ClientEvents&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;interactionCreate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Locale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// simplify discord's locale to something more basic (this is not needed, you can also use discord's locale-system which has more languages but is more of a headache to localize)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validLocale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;baseLocale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;localeStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;validLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is important to note, when loading interaction handlers from their respective file, we need to wrap them in &lt;code&gt;interactionMiddleware&lt;/code&gt; to get it working.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// project/src/index.ts&lt;/span&gt;
&lt;span class="c1"&gt;// Very basic example, you want to integrate this into your own loading logic!&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execute&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./commands/someCommand.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;interactionCreate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;interactionMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Localizing event handlers is also possible, but requires a more complex setup. More on that later.&lt;/p&gt;

&lt;p&gt;Now you can use paraglide's compiled messages in your command handler!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// project/src/commands/someCommand.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ChatInputCommandInteraction&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;discord.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../paraglide/messages.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Command declaration here...&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChatInputCommandInteraction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Ephemeral&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;example_message&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you're good to go! You have localized your Discord bot interactions!&lt;/p&gt;

&lt;h2&gt;
  
  
  Localizing Event Handlers
&lt;/h2&gt;

&lt;p&gt;You can apply the same pattern to event handlers (e.g., &lt;code&gt;messageCreate&lt;/code&gt;, &lt;code&gt;guildMemberAdd&lt;/code&gt;), but you’ll need to determine the locale manually since most events don’t include &lt;code&gt;interaction.locale&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strategies:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In-guild events&lt;/strong&gt;: Use &lt;code&gt;guild.preferredLocale&lt;/code&gt; (if available).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User-specific events&lt;/strong&gt;: Store user language preferences in your database and cache it to avoid unnecessary latency and database queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback&lt;/strong&gt;: Default to &lt;code&gt;baseLocale&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Example Middleware for &lt;code&gt;messageCreate&lt;/code&gt;:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/middleware/eventLocale.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AsyncLocalStorage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;async_hooks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;baseLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Locale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../paraglide/runtime.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;localeStorage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;AsyncLocalStorage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Locale&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Reuse the same instance everywhere&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;withEventLocale&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;getLocaleFromEvent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Locale&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getLocaleFromEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validLocale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;baseLocale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;localeStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;validLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;It is important, that the same AsyncLocalStorage instance is used for all middlewares&lt;/strong&gt; because we have overridden the get-logic before!&lt;/p&gt;

&lt;p&gt;Then in your event-loading-logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// wherever you're loading your events&lt;/span&gt;
&lt;span class="c1"&gt;// Very basic implementation - adjust for your own loading-logic&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;handleMessage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./events/messageCreate/index.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;withEventLocale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./middleware/eventLocale.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;baseLocale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./paraglide/runtime.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;messageCreate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;withEventLocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;guild&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;preferredLocale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;guild&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preferredLocale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Locale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Optional: check user DB, etc.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;baseLocale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Translation Compilation
&lt;/h3&gt;

&lt;p&gt;Paraglide compiles your messages to something useful in &lt;code&gt;project/src/paraglide&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You need to incorporate the compilation-step in your workflow somehow.&lt;br&gt;&lt;br&gt;
If using Docker, you can put &lt;code&gt;bun run compile-translations&lt;/code&gt; for example.&lt;/p&gt;

&lt;p&gt;When using a script like &lt;code&gt;start&lt;/code&gt; in your &lt;code&gt;package.json&lt;/code&gt;, you need to modify it by adding the compilation script before the actual run-step. Example for bun:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bun run compile-translations &amp;amp;&amp;amp; bun run src/index.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"compile-translations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"paraglide-js compile --project ./project.inlang --outdir ./src/paraglide"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Machine Translations
&lt;/h3&gt;

&lt;p&gt;Machine Translations are there to help you quickly translate your messages. They are not 100% accurate and might have some formatting issues. However, it gets the job done.&lt;/p&gt;

&lt;p&gt;When initializing paraglide, it adds the inlang CLI to your project as well as this script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"machine-translate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"inlang machine translate --project project.inlang"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can run this with your package manager of choice. With bun: &lt;code&gt;bun machine-translate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This will&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pull the &lt;code&gt;locales&lt;/code&gt; from your &lt;code&gt;project/project.inlang/settings.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Translate every message that is missing in other locales from your &lt;code&gt;baseLocale&lt;/code&gt; into all other locales
This will not overwrite existing translated messages and only translate that, where no translation has been found.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Links
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://inlang.com/m/gerre34r/library-inlang-paraglideJs" rel="noopener noreferrer"&gt;Paraglide Docs&lt;/a&gt; (if the link isn't valid, go &lt;a href="https://inlang.com/c/apps" rel="noopener noreferrer"&gt;here&lt;/a&gt; and look for &lt;strong&gt;Paraglide JS&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discordjs.guide/" rel="noopener noreferrer"&gt;Discord.js Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>discord</category>
      <category>inlang</category>
    </item>
    <item>
      <title>My WordPress Disaster: A Cautionary Tale (and a Lesson I Swear I’ve Learned Before)</title>
      <dc:creator>LukeZ</dc:creator>
      <pubDate>Mon, 25 Aug 2025 13:54:15 +0000</pubDate>
      <link>https://dev.to/thelukez/my-wordpress-disaster-a-cautionary-tale-and-a-lesson-i-swear-ive-learned-before-28g6</link>
      <guid>https://dev.to/thelukez/my-wordpress-disaster-a-cautionary-tale-and-a-lesson-i-swear-ive-learned-before-28g6</guid>
      <description>&lt;p&gt;They say you should never change a running system. Today, I can confirm that "they" are absolutely right. And that I, apparently, have the memory of a goldfish.&lt;/p&gt;

&lt;p&gt;Let me set the scene. I’m a newbie in the WordPress world, getting my feet wet with a local development environment. I had MAMP running, which came with its own version of PHP and MySQL. Everything was humming along nicely, and I was making progress styling and editing the child-theme of a theme we bought.&lt;/p&gt;

&lt;p&gt;Then, a new project came along. A different project, one that didn't need MAMP (with Symfony). So, in a fit of efficiency (or, as it turns out, pure hubris), I decided to delete MAMP and install PHP and MySQL separately using Homebrew. "Clean," I thought. "Efficient," I thought. "What could possibly go wrong?" I thought.&lt;/p&gt;

&lt;p&gt;What could go wrong, indeed. Turns out, WordPress isn’t just a pile of files and themes. It's a clingy little digital creature that stores its entire existence - every page, every setting, every glorious bit of config - inside a MySQL database. And where did MAMP, in its infinite wisdom, keep that precious database? Right there in its folder in the Application directory. The same directory I proceeded to “pulverize” (my new favorite word for this kind of digital annihilation) when I deleted MAMP.&lt;/p&gt;

&lt;p&gt;Fast forward to today. Time to show off our progress! I installed XAMPP, thinking it would be a quick fix. (Spoiler: It was not.) After an hour of pulling my hair out, the terrible truth dawned on me. The database. It was gone. Wordpress didn't find the database connection. All of it. Poof. Vanished. The only thing left were the theme files, like a ghostly, empty skeleton of a website.&lt;/p&gt;

&lt;p&gt;So, here I am, about to rebuild an entire homepage from scratch. But hey, at least I got a valuable life lesson out of it. One that I've learned about 50 times already, and will probably learn again many more times.&lt;/p&gt;

&lt;p&gt;Lesson of the day, kids: &lt;strong&gt;Never. Change. A. Running. System. EVER.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now, if you’ll excuse me, I have a date with a blank WordPress dashboard.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>mysql</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I'll definitely use this</title>
      <dc:creator>LukeZ</dc:creator>
      <pubDate>Mon, 16 Jun 2025 10:04:34 +0000</pubDate>
      <link>https://dev.to/thelukez/ill-definitely-use-this-3519</link>
      <guid>https://dev.to/thelukez/ill-definitely-use-this-3519</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/devopsdaily/how-to-structure-your-devops-side-project-for-real-world-deployments-1m84" class="crayons-story__hidden-navigation-link"&gt;How to Structure Your DevOps Side Project for Real-World Deployments&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/devopsdaily" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F382434%2F3b4f7f10-38d4-4f4f-8351-1dcb0c1bdfc7.png" alt="devopsdaily profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/devopsdaily" class="crayons-story__secondary fw-medium m:hidden"&gt;
              DevOps Daily
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                DevOps Daily
                
              
              &lt;div id="story-author-preview-content-2596173" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/devopsdaily" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F382434%2F3b4f7f10-38d4-4f4f-8351-1dcb0c1bdfc7.png" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;DevOps Daily&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/devopsdaily/how-to-structure-your-devops-side-project-for-real-world-deployments-1m84" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 16 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/devopsdaily/how-to-structure-your-devops-side-project-for-real-world-deployments-1m84" id="article-link-2596173"&gt;
          How to Structure Your DevOps Side Project for Real-World Deployments
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/docker"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;docker&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/beginners"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;beginners&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/cicd"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;cicd&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/devopsdaily/how-to-structure-your-devops-side-project-for-real-world-deployments-1m84" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;13&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/devopsdaily/how-to-structure-your-devops-side-project-for-real-world-deployments-1m84#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>devops</category>
      <category>docker</category>
      <category>beginners</category>
      <category>cicd</category>
    </item>
    <item>
      <title>My Late-Night Server Migration Saga</title>
      <dc:creator>LukeZ</dc:creator>
      <pubDate>Mon, 16 Jun 2025 10:01:41 +0000</pubDate>
      <link>https://dev.to/thelukez/my-late-night-server-migration-saga-51ji</link>
      <guid>https://dev.to/thelukez/my-late-night-server-migration-saga-51ji</guid>
      <description>&lt;p&gt;&lt;em&gt;A Tale of "Simple" Tasks That Spiral Out of Control&lt;/em&gt;&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;Disclaimer: I replaced the real domain and names in this post with placeholders because it doesn't matter; I also enhanced some paragraphs of my tale with AI because my English sentence structures are weird sometimes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yesterday evening, a seemingly "little bit" of refactoring for my Discord Bot's homepage turned into an unexpected adventure. My goal was straightforward: to adapt the existing codebase to new database types and fix a few minor bugs. After a quick push to the server, all seemed well.&lt;/p&gt;

&lt;p&gt;Or so I thought.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Mistake
&lt;/h2&gt;

&lt;p&gt;In the typical fashion of a late-night coding session, I completely overlooked a crucial detail: I had tweaked the &lt;code&gt;/stats&lt;/code&gt; API endpoint, a change that, unbeknownst to me, directly impacted my bot's homepage, residing in an entirely separate project. A swift fix to the endpoint URL and the homepage was back in action. Crisis averted, or so it appeared.&lt;/p&gt;

&lt;p&gt;But then, the clock ticked past 11 p.m. A dangerous hour for developers, where a fleeting thought can blossom into an ambitious project. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"It's still early," I reasoned, "plenty of time for more changes."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My long-standing desire to clean up my VPS servers resurfaced with newfound urgency. "Let's tackle that now!"&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feu7cx609pfzwsghb8x2a.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feu7cx609pfzwsghb8x2a.gif" alt="" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Simple" Plan
&lt;/h2&gt;

&lt;p&gt;A brief examination of my server landscape revealed a surprising fact: I could retire an expensive server entirely by migrating the homepage service to a less utilized machine. &lt;strong&gt;Sounds easy, right? ...Right?&lt;/strong&gt; The simplicity of the idea was almost alluring.&lt;/p&gt;

&lt;p&gt;I quickly pieced together a "master plan" that seemed incredibly straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new user account on the target server and grant necessary sudo permissions&lt;/li&gt;
&lt;li&gt;Set up Node.js and Nginx&lt;/li&gt;
&lt;li&gt;Clone the website repository&lt;/li&gt;
&lt;li&gt;Configure the essential &lt;code&gt;.env&lt;/code&gt; file for credentials&lt;/li&gt;
&lt;li&gt;Start the damn thing&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;"This sounds pretty straight forward and really simple,"&lt;/em&gt; I mused. &lt;em&gt;"What could possibly go wrong?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvvhvs9gwdcwckt0b3u36.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvvhvs9gwdcwckt0b3u36.gif" alt="Too easy" width="498" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spiral of "Simple" Tasks
&lt;/h2&gt;

&lt;p&gt;Right after creating that new user, a thought hit me: this server would be publicly accessible, so securing my login with an SSH key was a must! I'd done this before, many times, so I figured it'd be a quick win. I was, of course, &lt;strong&gt;wrong yet again&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  SSH Key Hell
&lt;/h3&gt;

&lt;p&gt;It had been months since I last generated an SSH key, and my memory of the exact command was, predictably, fuzzy. A quick search for a guide cleared that up. But, being me, I then decided to ditch the guide and "wing it." In hindsight, that was a mistake.&lt;/p&gt;

&lt;p&gt;Getting the key onto the server was easy enough. The real headache started when I tried to configure Termius (my SSH client) to use this new key. Despite having done it before, it was a total nightmare. Termius kept asking for a passphrase I never set and spat out a frustrating "pubkey failed" error. Why?! I was sure I'd done everything the same way as last time... or had I?&lt;/p&gt;

&lt;p&gt;To this day, I'm not entirely sure what the magic fix was. After regenerating the keys, reconfiguring Termius, and even recreating the user account, it just... worked. Somehow.&lt;/p&gt;

&lt;p&gt;By then, 30 minutes had vanished – it was already around 11:30 p.m. – and things were feeling far less "straightforward" than my initial plan suggested.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffqpqx6hhlznnjestugma.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffqpqx6hhlznnjestugma.gif" alt="Head bang againt table" width="224" height="126"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Point of No Return
&lt;/h3&gt;

&lt;p&gt;Then, for reasons only the universe knows, I decided to deviate from my original plan. I took the old homepage offline &lt;strong&gt;before&lt;/strong&gt; the new one was even close to running. Thankfully, I'd activated a maintenance page on my status monitoring, so at least I wouldn't be barraged with error notifications.&lt;/p&gt;

&lt;p&gt;With the old system down, I went to clone the website repo on the new server only to realize: &lt;strong&gt;nothing was installed yet!&lt;/strong&gt; No Node.js, no npm. A quick visit to the Node.js homepage for their install scripts, and I was back on track. Repository cloned, I thought, "You know what, I can quickly migrate this project from npm to pnpm. I've done it a few times recently; it'll be fast."&lt;/p&gt;

&lt;p&gt;And surprisingly, it was quick, taking about five minutes. I pulled the changes to the server, feeling a small surge of victory.&lt;/p&gt;

&lt;p&gt;Then another thought came into my mind: &lt;strong&gt;NGINX wasn't configured yet.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh3vhiozdfnonu0rzs0la.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh3vhiozdfnonu0rzs0la.gif" alt="Pressing big red button" width="498" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Missing Server
&lt;/h3&gt;

&lt;p&gt;My immediate thought was to just copy the config file, SSL certificate, and key from the old server. Easy, right?&lt;/p&gt;

&lt;p&gt;Wait... where did the old server go?&lt;/p&gt;

&lt;p&gt;Turns out, in my late-night haze, after taking down the old system, I'd also deleted its host entry from Termius. I had no idea where I'd saved those SSH credentials, and finding them now would take too long. But, I knew what a basic NGINX config looked like, so I quickly wrote it from scratch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# thedomain.dev.conf&lt;/span&gt;
&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="s"&gt;[::]:80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;thedomain.dev&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt; &lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="nv"&gt;$host$request_uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="s"&gt;[::]:443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;thedomain.dev&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;ssl_certificate&lt;/span&gt; &lt;span class="n"&gt;/etc/ssl/thedomain.dev/cert.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_certificate_key&lt;/span&gt; &lt;span class="n"&gt;/etc/ssl/thedomain.dev/key.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_protocols&lt;/span&gt; &lt;span class="s"&gt;TLSv1.2&lt;/span&gt; &lt;span class="s"&gt;TLSv1.3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;error_log&lt;/span&gt; &lt;span class="n"&gt;/var/log/nginx/thedomain.dev.error.log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="n"&gt;/var/log/nginx/thedomain.dev.access.log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://localhost:6060&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But then, the chilling realization: I'd need to generate new SSL certificates and reconfigure Cloudflare for the new server. &lt;strong&gt;Double crap.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fld0c6mhcoeq1l6g0of57.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fld0c6mhcoeq1l6g0of57.gif" alt="Looking around" width="498" height="247"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Midnight Panic
&lt;/h2&gt;

&lt;p&gt;The homepage had now been down for about 30 minutes, and the clock showed &lt;strong&gt;midnight&lt;/strong&gt;. Stress was building, and sleepiness was setting in fast.&lt;/p&gt;

&lt;p&gt;I raced through deleting and creating a new Cloudflare certificate for the domain, then switched Cloudflare to Development mode so DNS and SSL changes would propagate in seconds, not minutes. I slapped the new certificates onto the server, updated the DNS record to point to the new IP, and held my breath. Everything seemed fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Vanishing Code
&lt;/h3&gt;

&lt;p&gt;Next, I wanted to test the website locally one last time. I went to open the website project on my computer, but... where was it? The folder wasn't where it had been just 30 minutes ago. Please don't be deleted...&lt;/p&gt;

&lt;p&gt;The only explanation: I must have accidentally deleted the folder a few minutes earlier while clearing out my trash bin. This wasn't just losing the code; it meant the &lt;code&gt;.env&lt;/code&gt; files were gone too. I had no environment variables left – the ones on the server were gone because I deleted the connection, and now my local copies were nuked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Panic started to set in.&lt;/strong&gt; I tried to regain control over the whole situation, to map out the disaster I'd created.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write new &lt;code&gt;.env&lt;/code&gt; files based on what variables the project needed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;???&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because it was already very late I couldn't think more than one step at a time - so I just started doing stuff.&lt;/p&gt;

&lt;p&gt;After cloning the repo locally again, I recreated the &lt;code&gt;.env&lt;/code&gt; files. It took another five minutes to find all the necessary variables.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq2sd80dhao8ziu06i1i6.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq2sd80dhao8ziu06i1i6.gif" alt="Opening an empty folder" width="352" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cloudflare Catastrophe
&lt;/h2&gt;

&lt;p&gt;One final local test, and then I saw it: the stats on the website were showing the old fallback data. How could that be?&lt;/p&gt;

&lt;p&gt;It turns out, I'd messed up even more things – mistakes that could have been avoided if I hadn't done all this in the middle of the night and just waited for the next day.&lt;/p&gt;

&lt;p&gt;On Cloudflare's side, I'd done two crucial things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deleted and created a new certificate&lt;/li&gt;
&lt;li&gt;Updated the IP for the new server&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The massive oversight?&lt;/strong&gt; I did both of these for &lt;code&gt;api.thedomain.dev&lt;/code&gt; instead of &lt;code&gt;thedomain.dev&lt;/code&gt; (@)!&lt;/p&gt;

&lt;p&gt;This meant the bot's API endpoints now had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Invalid SSL certificates&lt;/li&gt;
&lt;li&gt;❌ Misconfigured DNS record&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was why the website couldn't access live data.&lt;/p&gt;

&lt;p&gt;It took me another 15 minutes to finally fix the DNS record to the correct IP and move the SSL certificates from the new server back to the original server where the bot and its API are running.&lt;/p&gt;

&lt;p&gt;Now it was already &lt;strong&gt;1:00 a.m.&lt;/strong&gt; The website had been down for over an hour, and the bot was limping because it relies on Discord's OAuth2 login flow, which uses the &lt;code&gt;api.thedomain.dev&lt;/code&gt; origin.&lt;/p&gt;

&lt;p&gt;My phone, meanwhile, was buzzing non-stop. The monitoring worker I'd set up to notify me via Telegram was doing its job, reminding me every 10 minutes of my boneheaded decisions for every service that was having issues. But I couldn't stop. I had to fix this mess, and quickly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnc31bch4e4o5o39nzbb0.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnc31bch4e4o5o39nzbb0.gif" alt="Big Facepalm" width="400" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Finish Line (and More Headaches)
&lt;/h2&gt;

&lt;p&gt;Finally, I checked &lt;code&gt;api.thedomain.dev&lt;/code&gt; one last time, and it was working perfectly again! With that behind me, I focused back on the new server. I generated the new SSL certificates needed for the main domain and put them in place. Then, I configured the DNS for the new server, confident I was almost done.&lt;/p&gt;

&lt;p&gt;But just when I thought I was in the clear, I hit another snag: for some reason, &lt;strong&gt;IPv6 just wasn't playing nice.&lt;/strong&gt; Not for the DNS record, and not even for SSH connections when I tested it. After all this, I just accepted it. I decided to stick to IPv4 for everything for now, making a mental note to dive into the IPv6 mystery later.&lt;/p&gt;

&lt;p&gt;By now, it was &lt;strong&gt;1:30 a.m.&lt;/strong&gt; and I was seriously wiped out. All I wanted was my bed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Final Facepalm
&lt;/h3&gt;

&lt;p&gt;Then, another facepalm moment: I realized I had also configured NGINX for the wrong origin earlier, setting it up for &lt;code&gt;api.thedomain.dev&lt;/code&gt; instead of &lt;code&gt;thedomain.dev&lt;/code&gt;. Thankfully, that was a quick fix, sorted out in about five minutes.&lt;/p&gt;

&lt;p&gt;With NGINX finally pointing correctly, I built the SvelteKit app and started it up. It was finally running!&lt;/p&gt;

&lt;p&gt;But... &lt;code&gt;https://thedomain.dev&lt;/code&gt; was... &lt;strong&gt;timing out?&lt;/strong&gt; What now?!&lt;/p&gt;

&lt;p&gt;I double-checked the NGINX config yet again. That's when it hit me: out of habit, I had only configured it to listen on IPv6. I'd been so used to working with IPv6 previously that I completely forgot to add the standard IPv4 listeners. The magical lines &lt;code&gt;listen 80;&lt;/code&gt; and &lt;code&gt;listen 443;&lt;/code&gt; were quickly added to the NGINX configuration and after I restarted the service, everything was &lt;strong&gt;finally was working&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgwaa2askw24jv5h6gn2s.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgwaa2askw24jv5h6gn2s.gif" alt="Crawling into the bed" width="640" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;After all that, it was &lt;strong&gt;2:00 a.m.&lt;/strong&gt; What was supposed to be a "quick" task had turned into a &lt;strong&gt;three-hour saga&lt;/strong&gt; of unforeseen errors, frantic debugging, and a growing sense of desperation.&lt;/p&gt;

&lt;p&gt;It was a harsh reminder that even the simplest-sounding IT tasks can unravel into a full-blown nocturnal adventure, especially when fatigue sets in.&lt;/p&gt;

&lt;p&gt;I should also note that &lt;a href="https://claude.ai" rel="noopener noreferrer"&gt;Claude.ai&lt;/a&gt; helped me a lot when finding the issues - I couldn't waste time for scraping websites so I used Claude to crawl the websites for giving me hints on what could be the cause and also some steps for configuration of NGINX.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lessons learned, the hard way, in the middle of the night.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Next time, I'm going to bed at 11 p.m. and tackling server migrations in the morning with a fresh cup of coffee and a clear head.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>webdev</category>
      <category>ubuntu</category>
      <category>migration</category>
    </item>
  </channel>
</rss>
