Skip to content

Commit 3cdd9b7

Browse files
committed
feat: use i18next based localizations
1 parent b147352 commit 3cdd9b7

File tree

14 files changed

+201
-92
lines changed

14 files changed

+201
-92
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$command": {
3+
"name": "avatar",
4+
"description": "This is an avatar command.",
5+
"options": [
6+
{
7+
"name": "user",
8+
"description": "The user to get the avatar for."
9+
}
10+
]
11+
},
12+
"avatar": "{{user}}'s avatar"
13+
}

apps/test-bot/src/app/locales/en-US/avatar.ts

-13
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$command": {
3+
"name": "avatar",
4+
"description": "C'est une commande pour afficher l'avatar.",
5+
"options": [
6+
{
7+
"name": "user",
8+
"description": "L'utilisateur dont vous souhaitez voir l'avatar."
9+
}
10+
]
11+
},
12+
"avatar": "Avatar de {{user}}"
13+
}

apps/test-bot/src/app/locales/fr/avatar.ts

-13
This file was deleted.

apps/test-bot/src/commands/old/legacy-autocomplete.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function run({ interaction }: SlashCommandProps) {
2727
const targetPetId = interaction.options.getString('pet');
2828
const targetPetObj = pets.find((pet) => pet.id === targetPetId);
2929

30-
interaction.reply(`Your pet name is ${targetPetObj.name}.`);
30+
interaction.reply(`Your pet name is ${targetPetObj!.name}.`);
3131
}
3232

3333
export function autocomplete({ interaction }: AutocompleteProps) {

packages/commandkit/src/cli/build.ts

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { join } from 'node:path';
1010
import { devEnvFileArgs, prodEnvFileArgs } from './env';
1111
import { rimraf } from 'rimraf';
1212
import { performTypeCheck } from './type-checker';
13+
import { copyLocaleFiles } from './common';
1314

1415
export interface ApplicationBuildOptions {
1516
plugins?: CompilerPlugin[];
@@ -80,6 +81,7 @@ export async function buildApplication({
8081
],
8182
});
8283

84+
await copyLocaleFiles('src', dest);
8385
await injectEntryFile(configPath || process.cwd(), !!isDev, config.distDir);
8486
} catch (error) {
8587
console.error('Build failed:', error);

packages/commandkit/src/cli/common.ts

+27
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,30 @@ async function ensureExists(loc: string) {
103103
export function erase(dir: string) {
104104
rimrafSync(dir);
105105
}
106+
107+
export async function copyLocaleFiles(_from: string, _to: string) {
108+
const resolvedFrom = join(process.cwd(), _from);
109+
const resolvedTo = join(process.cwd(), _to);
110+
111+
const localePaths = ['app/locales'];
112+
const srcLocalePaths = localePaths.map((path) => join(resolvedFrom, path));
113+
const destLocalePaths = localePaths.map((path) => join(resolvedTo, path));
114+
115+
for (const localePath of srcLocalePaths) {
116+
if (!fs.existsSync(localePath)) {
117+
continue;
118+
}
119+
120+
// copy localePath to destLocalePath
121+
const destLocalePath = destLocalePaths[srcLocalePaths.indexOf(localePath)];
122+
123+
if (!fs.existsSync(destLocalePath)) {
124+
fs.promises.mkdir(destLocalePath, { recursive: true });
125+
}
126+
127+
await fs.promises.cp(localePath, destLocalePath, {
128+
recursive: true,
129+
force: true,
130+
});
131+
}
132+
}

packages/commandkit/src/cli/development.ts

+2
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ export async function bootstrapDevelopmentServer(configPath?: string) {
152152
const isConfigUpdate = (path: string) => {
153153
const isConfig = configPaths.some((configPath) => path === configPath);
154154

155+
if (!isConfig) return false;
156+
155157
console.log(
156158
colors.yellowBright(
157159
'It seems like commandkit config file was updated, please restart the server manually to apply changes.',

packages/devtools/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"@types/express": "^5.0.1",
1515
"commandkit": "workspace:*",
1616
"discord.js": "^14.17.3",
17-
"tsconfig": "workspace:*"
17+
"tsconfig": "workspace:*",
18+
"typescript": "^5.8.3"
1819
},
1920
"files": [
2021
"dist",

packages/i18n/README.md

+86
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,89 @@ export default defineConfig({
2020
plugins: [i18n()]
2121
})
2222
```
23+
24+
You can pass options to the plugin:
25+
26+
```ts
27+
import { defineConfig } from "commandkit"
28+
import { i18n } from "@commandkit/i18n"
29+
30+
export default defineConfig({
31+
plugins: [
32+
i18n({
33+
plugins: [i18nextPlugins]
34+
})
35+
]
36+
})
37+
```
38+
39+
Create `locales` directory inside `src/app` and add your translation files. The directory structure should look like this:
40+
41+
```
42+
src
43+
└── app
44+
├── locales
45+
│ ├── en-US
46+
│ │ └── ping.json
47+
│ └── fr
48+
│ └── ping.json
49+
└── commands
50+
└── ping.ts
51+
```
52+
53+
CommandKit automatically localizes your commands if you follow the naming convention and translation file structure.
54+
55+
If your translation file contains `$command` key with localization object, it will be used to localize the command name and description.
56+
57+
```json
58+
{
59+
"$command": {
60+
"name": "Ping",
61+
"description": "Ping the server",
62+
"options": [
63+
{
64+
"name": "database",
65+
"description": "Ping the database"
66+
}
67+
]
68+
},
69+
"response": "Pong! The latency is {{latency}}ms"
70+
}
71+
```
72+
73+
The `$command` key defines localization for the command name and description (or options). These properties are later merged with the actual command to build the final command object with localizations that Discord understands. Anything else in the translation file is used to localize the command response.
74+
75+
This plugin adds `locale()` function to your command context. You can use it to localize your command responses.
76+
77+
```ts
78+
export const chatInput: SlashCommand = async (ctx) => {
79+
// ctx.locale() auto infers the localization of the current guild
80+
// you can also pass a discord.js locale enum to use custom locale
81+
// ctx.locale("fr") // uses french locale
82+
const { t, i18n } = ctx.locale()
83+
// ^ TFunction
84+
// ^ i18next instance
85+
86+
ctx.interaction.reply({
87+
content: t("response", { latency: client.ping }),
88+
ephemeral: true
89+
})
90+
}
91+
```
92+
93+
Additionally, you can use the `locale()` function outside of the context to get the localization api. This is mostly useful if you are using `i18n` with legacy commands plugin.
94+
95+
```ts
96+
import { locale } from "@commandkit/i18n"
97+
98+
export async function run({ interaction }) {
99+
const { t } = locale()
100+
101+
return interaction.reply({
102+
content: t("response", { latency: client.ping }),
103+
ephemeral: true
104+
})
105+
}
106+
```
107+
108+
This function is identical to the one in the context and it can also infer the locale automatically.

packages/i18n/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@commandkit/i18n",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"description": "CommandKit plugin that adds command localizations backed by i18next",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
@@ -36,4 +36,4 @@
3636
"tsconfig": "workspace:*",
3737
"typescript": "^5.7.3"
3838
}
39-
}
39+
}

0 commit comments

Comments
 (0)