Skip to content

Commit 1be0c81

Browse files
committed
Adding translation support and english
1 parent 656dd44 commit 1be0c81

18 files changed

+351
-94
lines changed

package.json

+6
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,14 @@
1010
"author": "",
1111
"license": "ISC",
1212
"dependencies": {
13+
"@types/node": "^20.4.6",
1314
"chalk": "4.1.2",
1415
"discord.js": "^14.11.0",
16+
"i18next": "^23.4.1",
17+
"js-yaml": "^4.1.0",
1518
"mongoose": "^7.4.0"
19+
},
20+
"devDependencies": {
21+
"@types/js-yaml": "^4.0.5"
1622
}
1723
}

src/commands/economy/daily.ts

+15-11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import RegisterEmbed from "../../utils/response/RegisterDialogue";
44
import UnregisteredEmbed from "../../utils/response/Unregistered";
55
import RegisterButton from "../../buttons/economy/register";
66
import ClaimedDaily from "../../utils/response/economy/ClaimedDaily";
7+
import i18next from "i18next";
78

89
export const data = new SlashCommandBuilder()
910
.setName('daily')
@@ -29,7 +30,7 @@ export async function execute(interaction: ChatInputCommandInteraction) {
2930

3031
if (lastDaily && lastDaily.time + 86400000 > Date.now()) {
3132
await interaction.reply({
32-
content: `You have already claimed your daily reward! You can claim it again <t:${Math.floor(lastDaily.time / 1000) + 86400}:R>.`,
33+
content: i18next.t('commands:economy.daily.alreadyClaimed', { cooldown: Math.floor(lastDaily.time / 1000) + 86400 }),
3334
ephemeral: true
3435
});
3536
return;
@@ -41,15 +42,18 @@ export async function execute(interaction: ChatInputCommandInteraction) {
4142

4243
const money = query.money
4344

44-
query.daily?.unshift({
45-
time: Date.now(),
46-
streak,
47-
amount: reward,
48-
money: {
49-
from: money,
50-
to: money + reward
51-
}
52-
});
45+
query.daily = [
46+
{
47+
time: Date.now(),
48+
streak,
49+
amount: reward,
50+
money: {
51+
from: money,
52+
to: money + reward
53+
}
54+
},
55+
...query.daily
56+
];
5357

5458
query.money += reward;
5559

@@ -61,7 +65,7 @@ export async function execute(interaction: ChatInputCommandInteraction) {
6165
});
6266
}).catch(async () => {
6367
await interaction.reply({
64-
content: 'An error occurred while trying to save your data. Please try again later.',
68+
content: i18next.t('commands:economy.daily.error.save'),
6569
ephemeral: true
6670
});
6771
});

src/config.ts.example

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export const APPLICATION_NAME = 'Tsuka';
22

3+
export const SYSTEM_LANGUAGE = 'en';
4+
35
export const APPLICATION_ID = 'YOUR_BOT_ID';
46
export const TOKEN = 'YOUR_BOT_TOKEN';
57

src/events/ready.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { Client, Events } from "discord.js";
22
import { log } from "../logger";
3-
import { Client as ClientEnum } from "../utils/logs";
3+
4+
import i18next from "i18next";
45

56
export const name = Events.ClientReady;
67
export const once = true;
78
export function execute(client: Client) {
89
if (!client.user) return;
9-
log(ClientEnum.READY, "Client", client.user.tag)
10+
log(i18next.t('system:client.ready', { tag: client.user.tag }), "Client")
1011
}

src/i18n.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import i18next from 'i18next';
2+
3+
import translation, { languages, namespaces } from './locales';
4+
5+
i18next.init({
6+
lng: 'en', // if you're using a language detector, do not define the lng option
7+
// debug: true,
8+
9+
fallbackLng: 'en',
10+
supportedLngs: languages,
11+
12+
ns: namespaces,
13+
14+
resources: Object.entries(translation).reduce((acc, [key, value]) => ({
15+
...acc,
16+
[key]: value
17+
}), {})
18+
});

src/index.ts

+18-11
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@ import { Client, Collection, Events, GatewayIntentBits, REST, Routes, SlashComma
22
import fs from 'node:fs';
33
import path from 'node:path';
44
import os from 'node:os';
5-
import { APPLICATION_ID, TOKEN } from './config'
5+
import { APPLICATION_ID, TOKEN, SYSTEM_LANGUAGE } from './config'
66

77
const [isLinux, isWindows] = ['Linux', 'Windows_NT'].map(x => os.type() === x);
88

99
import './utils/fillString';
1010
import { log } from './logger';
11-
import { Commands, Events as EventsLogs } from './utils/logs';
11+
12+
import './i18n';
13+
14+
import i18next from 'i18next';
15+
16+
if (SYSTEM_LANGUAGE !== i18next.language && i18next.languages.includes(SYSTEM_LANGUAGE)) {
17+
i18next.changeLanguage(SYSTEM_LANGUAGE);
18+
}
1219

1320
import './mongo';
1421

@@ -17,10 +24,10 @@ const client = new Client({ intents: [GatewayIntentBits.Guilds] }) as Client & {
1724
const commandsPath = path.join(__dirname, 'commands');
1825
const categories = fs.readdirSync(commandsPath)
1926

20-
log(Commands.POTENTIAL_COMMAND_CATEGORIES, 'Client', categories.length);
27+
log(i18next.t('system:commandsLoader.potentialCommandCategories'), 'Client', categories.length);
2128

2229
const commandCategories = categories.filter(category => fs.statSync(path.join(commandsPath, category)).isDirectory());
23-
log(Commands.FOUNDED_COMMAND_CATEGORIES, 'Client', commandCategories.length);
30+
log(i18next.t('system:commandsLoader.foundedCommandCategories'), 'Client', commandCategories.length);
2431

2532
let commands: any[] = [];
2633

@@ -33,7 +40,7 @@ function loadCategory(index: number) {
3340

3441
const category = commandCategories[index];
3542

36-
log(Commands.LOADING_CATEGORY, 'Category', index + 1, commandCategories.length, category);
43+
log(i18next.t('system:commandsLoader.loadingCategory', { category }), 'Category', i18next.t('system:rater', { current: index + 1, total: commandCategories.length }));
3744

3845
const categoryPath = path.join(commandsPath, category);
3946
const loadedCommands = fs.readdirSync(categoryPath).filter(file => /\.(ts|js)$/.test(file));
@@ -45,7 +52,7 @@ function loadCategory(index: number) {
4552
process.stdout.cursorTo(0);
4653
}
4754

48-
log(Commands.LOADING_COMMAND, 'Command', i + 1, loadedCommands.length, commandCategories[index], commandName);
55+
log(i18next.t('system:commandsLoader.loadingCommand', { category: commandCategories[index], command: commandName }), 'Command', i18next.t('system:rater', { current: i + 1, total: loadedCommands.length }));
4956

5057

5158
const commandPath = path.join(categoryPath, commandName);
@@ -63,14 +70,14 @@ function loadCategory(index: number) {
6370
const rest = new REST().setToken(TOKEN);
6471
(async () => {
6572
try {
66-
log(Commands.REFRESHING_APPLICATION_COMMANDS, 'REST', commands.length);
73+
log(i18next.t('system:commandsLoader.refreshingApplicationCommands'), 'REST', commands.length);
6774

6875
const data = await rest.put(
6976
Routes.applicationCommands(APPLICATION_ID),
7077
{ body: commands }
7178
) as any[];
7279

73-
log(Commands.RELOADED_APPLICATION_COMMANDS, 'REST', data.length);
80+
log(i18next.t('system:commandsLoader.reloadedApplicationCommands'), 'REST', data.length);
7481
} catch (error) {
7582
console.error(error);
7683
}
@@ -79,12 +86,12 @@ function loadCategory(index: number) {
7986
} else {
8087
process.stdout.clearLine(0);
8188
process.stdout.cursorTo(0);
82-
log(Commands.INVALID_COMMAND, 'Command', i + 1, loadedCommands.length, commandName);
89+
log(i18next.t('system:commandsLoader.invalidCommand'), 'Command', i18next.t('system:rater'), i + 1, loadedCommands.length, commandName);
8390
}
8491
} else {
8592
process.stdout.clearLine(0);
8693
process.stdout.cursorTo(0);
87-
log(Commands.INVALID_COMMAND, 'Command', i + 1, loadedCommands.length, commandName);
94+
log(i18next.t('system:commandsLoader.invalidCommand'), 'Command', i18next.t('system:rater'), i + 1, loadedCommands.length, commandName);
8895
}
8996
});
9097

@@ -96,7 +103,7 @@ function loadCategory(index: number) {
96103
async function loadEvent(index: number) {
97104
const eventName = eventFiles[index];
98105

99-
log(EventsLogs.LOADING_EVENT, 'Event', index + 1, eventFiles.length, eventName);
106+
log(i18next.t('system:eventsLoader.loadingEvent', { event: eventName }), 'Event', i18next.t('system:rater', { current: index + 1, total: eventFiles.length }));
100107

101108
const eventPath = path.join(eventsPath, eventName);
102109

src/locales/en/commands.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
economy:
2+
daily:
3+
alreadyClaimed: "You have already claimed your daily reward! You can claim it again <t:{{cooldown}}:R>."
4+
error:
5+
save: "An error occurred while trying to save your data. Please try again later."

src/locales/en/embeds.yaml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
unregistered:
2+
title: "You are not registered!"
3+
description:
4+
- "You are not registered!"
5+
- "Please register using `/register` command!"
6+
thumbnail: "https://i.imgur.com/AfFp7pu.png"
7+
register:
8+
title: "Failure to abide by or violate the following rules may result as account reset or ban."
9+
author: "{{name}} Bot Rules"
10+
description:
11+
firstLine: "* It banned everything that provides an additional advantage over other users. These include but are not limited to macros, auto-farmers, and auto-anything."
12+
secondLine: "* Don't use exploits found in the bot. Report them immediately."
13+
thirdLine: "* In-game currency can only be converted to non-bot resources using the official system. We forbid all 3rd party services and currency trading for anything good outside of the bot."
14+
fourthLine: "*By clicking the emoji at the bottom, you agree to accept all terms and conditions and accept responsibility for your actions.*"
15+
footer: "{{count}} users agreed"
16+
alreadyRegistered:
17+
title: "You are already registered!"
18+
description: "You are already registered!\nYou cannot register again!"
19+
thumbnail: "https://i.imgur.com/AfFp7pu.png"
20+
claimedDaily:
21+
title: "Successfully claimed your daily reward!"
22+
description: "You have claimed your daily reward!\nYou have claimed {{amount}} coins**!\nYour streak is now **{{streak}}**!"
23+
thumbnail: "https://i.imgur.com/AfFp7pu.png"
24+
balance:
25+
title: "Heres your balance!"
26+
description: "You have **{{balance}} coins**!"
27+
thumbnail: "https://i.imgur.com/AfFp7pu.png"

src/locales/en/system.yaml

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
rater: "\x1B[90m\x1B[1m[\x1B[22m\x1B[39m\x1B[32m\x1B[1m{{current}}\x1B[22m\x1B[39m\x1B[90m\x1B[1m/\x1B[22m\x1B[39m\x1B[32m\x1B[1m{{total}}\x1B[22m\x1B[39m\x1B[90m\x1B[1m]\x1B[22m\x1B[39m"
2+
3+
commandsLoader:
4+
potentialCommandCategories: "Founded potential $0 command categories."
5+
foundedCommandCategories: "Founded $0 command categories."
6+
7+
loadingCategory: "$0 Loading category '{{category}}'..."
8+
loadingCommand: " $0 Loading command '{{category}}/{{command}}'..."
9+
invalidCommand: " $0 \x1B[31m\x1B[1mInvalid command '$3'!\x1B[22m\x1B[39m"
10+
11+
refreshingApplicationCommands: "Started refreshing $0 application (/) commands."
12+
reloadedApplicationCommands: "Successfully reloaded $0 application (/) commands."
13+
eventsLoader:
14+
loadingEvent: "$0 Loading event '{{event}}'..."
15+
client:
16+
ready: "\x1B[32m\x1B[1mReady! Logged in as\x1B[22m\x1B[39m \x1B[34m\x1B[1m{{tag}}\x1B[22m\x1B[39m"
17+
database:
18+
connected: "\x1B[32m\x1B[1mSuccessfully connected to database.\x1B[22m\x1B[39m"
19+
disconnected: "\x1B[32m\x1B[1mSuccessfully \x1B[22m\x1B[39m\x1B[31m\x1B[1mdisconnected\x1B[22m\x1B[39m\x1B[32m\x1B[1m from database.\x1B[22m\x1B[39m"
20+
error: "\x1B[31mAn error occurred while connecting to database.\x1B[39m \x1B[31m\x1B[1m$0\x1B[22m\x1B[39m"

src/locales/index.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
4+
import yaml from 'js-yaml';
5+
6+
const languages = fs.readdirSync(path.join(__dirname)).filter(
7+
name => /[a-z]{2}/.test(name) && fs.statSync(path.join(__dirname, name)).isDirectory()
8+
);
9+
10+
const namespaces = fs.readdirSync(path.join(__dirname, languages[0])).filter(
11+
name => /\.yaml$/.test(name)
12+
).map(name => name.replace(/\.yaml$/, ''));
13+
14+
export {
15+
languages,
16+
namespaces
17+
}
18+
19+
const translation = languages.reduce((acc, dir) => {
20+
const files = fs.readdirSync(path.join(__dirname, dir)).filter(
21+
name => /\.yaml$/.test(name)
22+
);
23+
24+
files.forEach(file => {
25+
const content = fs.readFileSync(path.join(__dirname, dir, file), 'utf-8');
26+
const parsed = yaml.load(content);
27+
28+
acc[dir][file.replace(/\.yaml$/, '')] = parsed;
29+
})
30+
31+
return acc;
32+
}, languages.reduce((cur, lang) => ({
33+
...cur,
34+
[lang]: {}
35+
}), {} as any) as {
36+
[key: string]: {
37+
[key: string]: any
38+
}
39+
});
40+
41+
export default translation;

src/mongo.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import mongoose from "mongoose";
22
import { MongoURL, MongoPassword } from "./config";
33
import { log } from "./logger";
4-
import { Database } from "./utils/logs";
54

5+
import i18next from "i18next";
66

77
mongoose.connect(MongoURL.replace('<password>', MongoPassword)).then(() => {
8-
log(Database.CONNECTED, "Database");
8+
log(i18next.t('system:database.connected'), "Database");
99
}).catch((err) => {
10-
log(Database.ERROR, "Database", err);
10+
log(i18next.t('system:database.error'), "Database", err);
1111
});

0 commit comments

Comments
 (0)