Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a6b5406
feat(i18n): add i18next infrastructure with locale support
RedsTom Jun 17, 2026
af63883
feat(i18n): wire up i18n in module loader, index, and config service
RedsTom Jun 17, 2026
7045c01
feat(i18n): migrate core messages to i18n with EN/FR translations
RedsTom Jun 17, 2026
554a125
feat(i18n): localize config handlers and scalar-list-editor
RedsTom Jun 17, 2026
a304dcb
feat(i18n): localize slash commands, interactions and listener
RedsTom Jun 17, 2026
ae18d96
feat(i18n): add module-level i18n for thread-creator and test-config
RedsTom Jun 17, 2026
2af8eef
docs(i18n): add localization sections to EN documentation
RedsTom Jun 17, 2026
7a9cad1
docs(i18n): add localization sections to FR documentation
RedsTom Jun 17, 2026
5a9241d
docs(i18n): add tip notes that TypeScript values default to English
RedsTom Jun 17, 2026
69f5009
docs(i18n): fix commands localization — names are not localizable on …
RedsTom Jun 17, 2026
0f38cb5
docs(i18n): fix command name localization note — Discord supports it,…
RedsTom Jun 17, 2026
d85a6cb
docs(i18n): add dedicated Localization page to sidebar
RedsTom Jun 17, 2026
0815343
docs(i18n): escape Vue interpolation in localization pages
RedsTom Jun 17, 2026
903b238
fix(core): restore missing 'Current:' prefix in config option display
RedsTom Jun 17, 2026
b677a77
refactor(core): extract core config helper to reduce repetition and b…
RedsTom Jun 17, 2026
1186442
docs: fix localization doc table values to match actual i18n keys
RedsTom Jun 17, 2026
f0f4cbf
feat(config): resolve config defaults lazily with per-locale i18n
AntoineJT Jun 17, 2026
2e922ea
feat(config): mark default values in the /config panel
AntoineJT Jun 17, 2026
dbfc71d
feat(config): render enum option labels via Intl language names
AntoineJT Jun 17, 2026
38505fe
feat(config): prefix the language select with flag emojis
AntoineJT Jun 17, 2026
57c8561
feat(config): add a field-level reset control to /config
AntoineJT Jun 17, 2026
aba0a82
refactor(core): rename the core module to "Global Configuration"
AntoineJT Jun 17, 2026
1483601
fix(config): mark every unset field as default, including empty lists
AntoineJT Jun 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/functional.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ Toujours actif, non désinstallable. Fournit la gestion des modules pour les adm

Affiche la liste de tous les modules disponibles avec leur statut sur le serveur (activé/désactivé), leur version activée, et leur description. Chaque module dispose d'un bouton **Activer** (vert) ou **Désactiver** (rouge) qui prend effet immédiatement.

### `/config` — Réinitialiser des champs

En bas du panneau `/config` d'un module, un bouton **Réinitialiser…** ouvre un sélecteur (éphémère, visible de l'administrateur seul) listant les champs actuellement personnalisés, plus une entrée **Tous les champs**. Les champs choisis sont remis à leur valeur par défaut.

Le bouton est désactivé tant qu'aucun champ n'a été personnalisé (rien à réinitialiser). Réinitialiser un champ retire simplement la valeur enregistrée : il repasse sur le défaut (qui suit alors de nouveau la langue du serveur, cf. plus bas) et son marqueur _(par défaut)_ réapparaît.

---

## Module Thread Creator
Expand All @@ -38,6 +44,15 @@ Dès qu'un message est posté dans le salon configuré :
**Template par défaut :** `Discussion - {messageAuthor}`
**Message de bienvenue par défaut :** `💬 Utilisez ce fil pour discuter de ce sujet !`

> [!NOTE]
> Les valeurs par défaut suivent la langue du serveur (`/config core`) : tant
> qu'un administrateur n'a pas saisi sa propre valeur, le message de bienvenue
> par défaut s'affiche dans la langue configurée et change si on bascule la
> langue. Dès qu'une valeur est définie manuellement, elle est figée et
> n'est plus affectée par la langue. Dans `/config`, une valeur encore par
> défaut est signalée par le marqueur _(par défaut)_, ce qui permet de voir
> d'un coup d'œil ce qui a réellement été configuré.

**Limites :**

- Noms de fils tronqués à 100 caractères (limite Discord)
Expand Down
2 changes: 2 additions & 0 deletions docs/site/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export default defineConfig({
{ text: "Écouteurs", link: "/fr/guide/listeners" },
{ text: "Interactions", link: "/fr/guide/interactions" },
{ text: "Configuration", link: "/fr/guide/configuration" },
{ text: "Localisation", link: "/fr/guide/localisation" },
{ text: "Services", link: "/fr/guide/services" },
{ text: "Base de données", link: "/fr/guide/database" },
{ text: "Contribuer", link: "/fr/guide/contributing" },
Expand Down Expand Up @@ -103,6 +104,7 @@ export default defineConfig({
{ text: "Listeners", link: "/en/guide/listeners" },
{ text: "Interactions", link: "/en/guide/interactions" },
{ text: "Configuration", link: "/en/guide/configuration" },
{ text: "Localization", link: "/en/guide/localization" },
{ text: "Services", link: "/en/guide/services" },
{ text: "Database", link: "/en/guide/database" },
{ text: "Contributing", link: "/en/guide/contributing" },
Expand Down
17 changes: 11 additions & 6 deletions docs/site/en/guide/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,21 @@ When the bot starts (`src/index.ts`), it follows this sequence:
└─ prisma.$queryRaw`SELECT 1`
└─ exits with error if DB is unavailable

2. Module discovery
2. i18n initialization
└─ initI18n() — initializes the i18next engine with fallback locale support
└─ loadModuleI18n("core") — loads core translation files (en.json, fr.json)

3. Module discovery
└─ loadModules("./modules")
└─ scans each subdirectory in src/modules/
└─ imports the *.module.ts file
└─ skips devOnly modules in production

3. Intent aggregation
4. Intent aggregation
└─ collects GatewayIntentBits from all modules
└─ creates the Discord Client with the union of all intents

4. ClientReady handler (async)
5. ClientReady handler (async)
├─ For each module:
│ ├─ module.onLoad(client, registry)
│ └─ loadModuleEvents(client, module)
Expand All @@ -31,10 +35,10 @@ When the bot starts (`src/index.ts`), it follows this sequence:
│ └─ Prod mode: version-gated guild commands + global core commands
└─ loadGlobalEvents(client)

5. Login
6. Login
└─ client.login(token)

6. Shutdown handlers
7. Shutdown handlers
└─ SIGTERM / SIGINT → client.destroy() + prisma.$disconnect()
```

Expand Down Expand Up @@ -122,7 +126,8 @@ The configuration system has several layers:
2. **Storage** — All configs are stored as a single JSON blob per guild in the `GuildConfiguration` table
3. **Cache** — `ConfigService` holds an in-memory cache (`configCache`) to avoid DB reads on every interaction
4. **Deserialization** — Entity IDs (user, role, channel) are stored as strings and resolved to Discord objects at read time
5. **Admin UI** — `/config <module>` renders an interactive panel with per-field edit controls
5. **Localization** — Field names and descriptions are resolved from the module's i18n files (falling back to English defaults in the TypeScript schema)
6. **Admin UI** — `/config <module>` renders an interactive panel with per-field edit controls

## Service Layer

Expand Down
27 changes: 27 additions & 0 deletions docs/site/en/guide/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ This guide explains how to create Discord slash commands in your modules. Comman

## Creating a Command

::: tip
Set command names and descriptions in English by default — they act as the fallback when no translation is available for the guild's locale. In this bot, only descriptions are localized via `setDescriptionLocalizations()`; command names stay in English.
:::

```typescript
// src/modules/greeter/commands/hello.command.ts

Expand Down Expand Up @@ -166,6 +170,29 @@ OmniBot provides two built-in commands in the **Core** module (always active, no
| `/modules` | Administrator | Lists all modules with enable/disable buttons |
| `/config <module>` | Administrator | Opens the interactive configuration panel for a module |

## Localization

Slash command descriptions can be translated per locale using Discord's built-in localization methods:

```typescript
data: new SlashCommandBuilder()
.setName("hello")
.setDescription("Says hello!")
.setDescriptionLocalizations({ fr: "Dit bonjour !" }),
```

> In this bot, only descriptions are localized. Command names stay in English.

For module-specific strings in command responses, use `config.t()` to look up translations from your module's i18n files:

```typescript
async execute(interaction, config) {
await interaction.reply(config.t("greeting", { user: interaction.user.username }));
}
```

The guild's locale is configured via the core module's `/config core > locale` setting. See [Configuration → Localization](./configuration#localization) for details on setting up translation files.

## Best Practices

- **Use reply, deferReply, or editReply** appropriately — defer for operations that take longer than 3 seconds
Expand Down
48 changes: 48 additions & 0 deletions docs/site/en/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ OmniBot provides a typed configuration system for modules. Each module can decla

A module declares its schema in `defineModule()` via the `config` property:

::: tip
Config field `name` and `description` must be in English in the TypeScript schema — they serve as the fallback when no translation file matches the guild's locale. No need to duplicate them in `en.json`. Add translations in `i18n/` files for other locales.
:::

```typescript
// src/modules/my-module/my-module.module.ts

Expand Down Expand Up @@ -177,6 +181,50 @@ model GuildConfiguration {
- An in-memory cache (`configCache`) avoids database reads on every interaction
- The cache is invalidated on every write

## Localization

Config field names and descriptions can be translated per guild locale via i18n files. The system resolves localized values automatically based on the guild's configured locale (`/config core > locale`).

### Module-level translations

Create an `i18n/` directory in your module with one JSON file per locale:

```json
// i18n/en.json
{
"config.myField.name": "My Field",
"config.myField.description": "Description of my field."
}

// i18n/fr.json
{
"config.myField.name": "Mon champ",
"config.myField.description": "Description de mon champ."
}
```

The field's `name` and `description` in the TypeScript schema serve as English defaults — there's no need to duplicate them in `en.json`.

### Using config.t() for messages

The `ConfigProvider` injected into commands, listeners, and interactions exposes a `t()` method for localized strings anywhere in your module:

```typescript
async execute(interaction, config) {
const message = config.t("welcomeMessage", { user: interaction.user.username });
await interaction.reply(message);
}
```

### Namespace fallback

Keys are resolved in this order:

1. Your module's namespace (e.g. `thread-creator:welcomeMessage`)
2. The core namespace (`core:welcomeMessage`)

This means common UI strings like `config.previous`, `config.next`, `modules.enable`, and type names (`type.text`, `type.number`, etc.) are provided by the core namespace — you only need to translate module-specific strings.

## Best Practices

- **Use camelCase for keys**, human-readable `name` and clear `description`
Expand Down
58 changes: 58 additions & 0 deletions docs/site/en/guide/creating-a-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ A module in OmniBot is a self-contained functional unit that can be installed an
```
src/modules/my-module/
├── my-module.module.ts # Main module definition
├── i18n/ # Translation files
│ ├── en.json
│ └── fr.json
├── commands/ # Slash commands
│ └── greet.command.ts
├── listeners/ # Event listeners
Expand All @@ -22,6 +25,10 @@ src/modules/my-module/

## Step-by-Step: Creating a "Greeter" Module

::: tip
The module's `name` and `description` should be in English by default. They serve as the fallback when no translation file matches the guild's locale. Add translations in `i18n/` files for other locales.
:::

### 1. Create the directory and main file

```typescript
Expand Down Expand Up @@ -142,6 +149,57 @@ The module loader (`src/core/loaders/module-loader.ts`) scans `src/modules/` at

**Dev-only modules** (with `devOnly: true`) are skipped when `NODE_ENV` is not `"development"`. Use this for test or debug modules.

## Internationalization

Modules can provide translations for their interface strings (name, description, config fields, and bot messages) using per-module locale files.

### Adding translations

Create an `i18n/` directory in your module with one JSON file per locale:

```
src/modules/my-module/
├── my-module.module.ts
├── i18n/
│ ├── en.json
│ └── fr.json
└── commands/
└── ...
```

The module loader auto-discovers these files at startup and registers them with i18next.

### Translation file format

Each file contains key-value pairs. The module's `name`, `description`, and config field `name`/`description` are automatically resolved from these files, overriding the TypeScript defaults when a matching locale is active:

```json
{
"module.name": "My Module",
"module.description": "Does awesome things.",
"config.myField.name": "My Field",
"config.myField.description": "Description of my field.",
"greeting": "Hello {{name}}!"
}
```

### Using translations in code

The `ConfigProvider` exposed to commands, listeners, and interactions provides a `t()` method:

```typescript
async execute(interaction, config) {
const greeting = config.t("greeting", { name: interaction.user.username });
await interaction.reply(greeting);
}
```

Keys are looked up in the module's own namespace first, then fall back to core translations. Common UI labels (`config.previous`, `config.next`, `modules.enable`, etc.) are provided by the core namespace — no need to redefine them in every module.

### Locale selection

The guild's locale is configured via the core module's settings (`/config core > locale`). When a locale file doesn't exist for the selected language, the system falls back to English.

## Best Practices

- **Keep `onLoad` lean** — register artifacts and log, move logic to services
Expand Down
5 changes: 4 additions & 1 deletion docs/site/en/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Welcome to OmniBot! This guide will help you set up the bot for development and

## What is OmniBot?

OmniBot is a **modular Discord bot** developed for the [Graven - Développement](https://discord.gg/graven) community. Each feature is a self-contained **module** that is auto-discovered at startup and can be installed or uninstalled **per Discord server** via `/modules`. Modules declare their own slash commands, event listeners, interaction handlers, and a typed configuration schema edited live with `/config <module>`.
OmniBot is a **modular Discord bot** developed for the [Graven - Développement](https://discord.gg/graven) community. Each feature is a self-contained **module** that is auto-discovered at startup and can be installed or uninstalled **per Discord server** via `/modules`. Modules declare their own slash commands, event listeners, interaction handlers, and a typed configuration schema edited live with `/config <module>`. The entire interface is translatable — each module can bundle its own locale files.

The core system wires everything together — adding a feature means creating a module, never touching the bootstrap.

Expand Down Expand Up @@ -98,7 +98,9 @@ src/
│ └── utils/ # Permission guard, version parser, messages
├── modules/ # Feature modules (one folder per module)
│ ├── thread-creator/ # Example module: automatic thread creation
│ │ └── i18n/ # Module translations (en.json, fr.json)
│ └── test-config/ # Dev-only: exercises every config type
│ └── i18n/ # Module translations (en.json, fr.json)
├── lib/ # Shared contracts exposed to modules
│ ├── module.ts # defineModule()
│ ├── command.ts # declareCommand()
Expand Down Expand Up @@ -134,6 +136,7 @@ Files use suffixes to identify their role:
| `*.select.ts` | Select menu handler |
| `*.service.ts` | Business service (e.g. `thread-creation-queue.service.ts`) |
| `*.prisma` | Prisma model definition |
| `i18n/*.json` | Locale translation files (e.g. `en.json`, `fr.json`) |

---

Expand Down
15 changes: 15 additions & 0 deletions docs/site/en/guide/interactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,21 @@ Interactions are automatically tied to module activation. When a module is disab

When editing configuration via ephemeral select menus, the source config message (public) needs to be updated. The system uses `refreshSourceConfigMessage()` which threads the source message ID through `customId` arguments and re-edits the public message after each change.

## Localization

Interaction responses can use `config.t()` for localized strings. Provide translations in your module's `i18n/` files:

```typescript
async execute(interaction, [actionId, userId]) {
await interaction.reply({
content: config.t("actionConfirmed", { action: actionId, user: userId }),
flags: MessageFlags.Ephemeral,
});
}
```

The guild's locale is configured via `/config core > locale`. See [Configuration → Localization](./configuration#localization) for details.

## Best Practices

### Response Types
Expand Down
13 changes: 13 additions & 0 deletions docs/site/en/guide/listeners.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,19 @@ export default defineModule({

Additionally, **privileged intents** (`GuildMembers`, `GuildPresences`, `MessageContent`) must be enabled in the Discord Developer Portal under Bot > Privileged Gateway Intents.

## Localization

Listener responses can use `config.t()` for localized strings. Provide translations in your module's `i18n/` files:

```typescript
async execute(message, config) {
if (!config) return;
await message.reply(config.t("welcomeMessage", { user: message.author.username }));
}
```

The guild's locale is configured via `/config core > locale`. See [Configuration → Localization](./configuration#localization) for details.

## Best Practices

- **Filter early** — check for bots, DMs, or irrelevant channels at the top of `execute()`
Expand Down
Loading