Skip to content

Localization

Popii picks up the user’s Discord language setting automatically and applies translations from your locale files — no manual locale detection code required.

Create JSON files in src/locales/ named after their IETF language tag:

src/locales/
en-US.json ← default fallback
fr.json
de.json
pt-BR.json

Each file is a flat key–value map:

src/locales/en-US.json
{
"greeting": "Hello, {{username}}!",
"balance": "You have {{amount}} coins.",
"error.not_found": "That item doesn't exist."
}
src/locales/fr.json
{
"greeting": "Bonjour, {{username}} !",
"balance": "Vous avez {{amount}} pièces.",
"error.not_found": "Cet objet n'existe pas."
}

Use pop.t() (or pop.translate() — they’re identical):

export default command({
name: "balance",
description: "Check your coins",
async do(pop) {
const coins = await getBalance(pop.user.id);
await pop.reply(pop.t("balance", { amount: coins }));
},
});

{{amount}} in the string is replaced with the value from the second argument. You can pass any number of tokens.

pop.locale is set from interaction.locale — the language the user has selected in Discord’s settings. Popii uses this to pick the right file automatically:

pop.locale // "en-US", "fr", "de", "pt-BR", etc.

If no file exists for the user’s locale, Popii falls back to the locales.default setting (defaults to "en-US"). If the key is also missing from the fallback file, the key itself is returned unchanged.

const client = popiiClient({
token: process.env.DISCORD_TOKEN!,
locales: {
dir: "./src/locales", // default — override if your files live elsewhere
default: "en-US", // fallback locale
},
});

Fetch translations from a URL at startup instead of bundling them in your project:

locales: {
url: "https://cdn.example.com/locales", // fetches /en-US.json, /fr.json, etc.
default: "en-US",
},

Popii fetches all available locale files from the URL on startup and caches them in memory. Useful for managing translations outside your codebase.

Keys are flat strings — use dots or slashes to create a naming convention for namespacing without any special config:

{
"command.ping.reply": "Pong! {{latency}}ms",
"command.help.title": "Available Commands",
"error.permission": "You don't have permission to do that.",
"error.cooldown": "Please wait {{seconds}} seconds before using this again."
}
await pop.reply(pop.t("command.ping.reply", { latency: ping }));

Discord supports translated slash command names and descriptions natively. Pass nameLocalizations and descriptionLocalizations to your command:

export default command({
name: "balance",
description: "Check your coins",
nameLocalizations: {
fr: "solde",
de: "guthaben",
},
descriptionLocalizations: {
fr: "Vérifiez vos pièces",
de: "Überprüfe deine Münzen",
},
async do(pop) { /* ... */ },
});

Discord will show the localized name to users in the matching locale.

pop.t() accepts any string as the key. If you want autocompletion and type-checking for your specific keys, generate a type from your locale file:

src/types.d.ts
import en from "./locales/en-US.json";
declare module "popii-framework" {
interface PopiiLocales {
keys: keyof typeof en;
}
}

Access the raw locale string when you need to format numbers, dates, or currencies for the user’s region:

async do(pop) {
const amount = 1_234_567.89;
const formatted = new Intl.NumberFormat(pop.locale).format(amount);
await pop.reply(`Amount: ${formatted}`); // "1,234,567.89" for en-US, "1 234 567,89" for fr
},

Event handlers receive EventPop, which has the same pop.t() and pop.locale as command handlers:

src/events/guildMemberAdd.ts
import { event } from "popii-framework";
export default event(async (pop, member) => {
const welcome = pop.t("greeting", { username: member.user.username });
await member.guild.systemChannel?.send(welcome);
});