Skip to content

feat: i18n support and localized configuration UX - #155

Merged
AntoineJT merged 23 commits into
masterfrom
feat/i18n
Jun 17, 2026
Merged

feat: i18n support and localized configuration UX#155
AntoineJT merged 23 commits into
masterfrom
feat/i18n

Conversation

@RedsTom

@RedsTom RedsTom commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Description

This PR adds full internationalization (i18n) support to the bot, then builds a localized configuration experience on top of it. Modules provide translation files, guild admins pick the bot language via /config (Global configuration →
Language), and the config UI is fully localized — including defaults, value labels and a reset flow.

Changes

i18n infrastructure

  • New #lib/i18n.ts wrapping i18next with namespace fallback to core
  • Module i18n auto-loader: i18n/<locale>.json files are loaded at startup
  • ConfigProvider exposes t() (per-guild locale) and locale
  • getConfigTypeName() accepts an optional TFunction for localized type names

Core translations

  • src/core/i18n/{en,fr}.json with all core UI strings
  • Locale config option (ENUM en/fr) in core.config.ts

Module translations

  • test-config/i18n/{en,fr}.json and thread-creator/i18n/{en,fr}.json
  • Config fields (name, description) and module metadata localizable

Config defaults (lazy + per-locale)

  • Defaults are no longer persisted: ConfigProvider.get() resolves them at read time, so an unset value tracks the guild locale and changes live
  • Free-text defaults are localizable via a config.<key>.default key (the schema defaultValue stays the English fallback)
  • No migration: guilds created before this keep their stored value until a reset

Config UI

  • Default marker — fields the admin hasn't set are flagged _(default)_ (config.defaultSuffix), backed by ConfigProvider.isSet()
  • Language labels — an enum may declare display: "language"; its options render as localized names via Intl.DisplayNames, with flag emojis, in both the readout and the select menu (no translation keys to maintain)
  • Field reset — a "Reset…" control opens an ephemeral picker listing the overridden fields plus an "All fields" entry; resetting clears the stored value so the lazy default takes over. Backed by ConfigService.resetFieldsForModuleIn()
  • Core module renamed Global Configuration / Configuration Globale, and the panel header reworded to Settings · {{module}} / Paramètres · {{module}}

Refactoring

  • Extracted src/core/utils/core-config.ts (getCoreT, getCoreLocale, replyWithCoreT) to centralize core config access and break circular deps
  • Config handlers/interactions use the helper instead of importing coreModule

Documentation

  • New docs/site/{en,fr}/guide/localization*.md (+ sidebar entries), covering module/field translations, localizable defaults, the default marker, the display: "language" hint and the reset flow
  • docs/functional.md updated for the language behavior, default marker and reset

Verification

  • pnpm build passes clean
  • All 134 tests pass (pnpm vitest run)
  • Code passes linting and formatting checks

RedsTom added 16 commits June 17, 2026 18:51
- Add i18next dependency for internationalization
- Create src/lib/i18n.ts (initI18n, addTranslations, createT)
- Create src/core/core.config.ts with locale ENUM (en/fr)
- Extend ConfigProvider with locale param and config.t() for key lookup
- Add getConfigTypeName() with optional TFunction for localized type names
- Init i18n before module loading in src/index.ts
- Add loadModuleI18n() to auto-discover module i18n/*.json files
- Load core i18n explicitly in core.module.ts
- Resolve locale per guild via getLocaleForGuild() in config service
- Propagate locale through all config service methods
- Add core translations (en.json + fr.json) for config UI, modules list,
  scalar list editor, modals, placeholders, and error messages
- Migrate all hardcoded strings in core-messages.ts to use coreConfig.t()
- Replace template literals with i18next named interpolation ({{param}})
- Update require-admin.ts to use localized messages
- Update core-messages.test.ts for new i18n-based output
- Migrate all 7 config handlers (string, number, enum, select, user,
  role, channel, category) to use config.t() with ns fallback
- Localize scalar-list-editor with i18n keys
- Update integration tests with mock for #core/core.module.js
  to prevent circular dependency
- Add setDescriptionLocalizations() for /config and /modules commands
- Localize autocomplete module names via config.t()
- Migrate all button interactions (config-page, configure-module,
  enable-module, disable-module, toggle-option) to use coreConfig.t()
- Update interaction-create.listener.ts for localized responses
- Migrate thread-creator config name/description to English defaults,
  add i18n/en.json + i18n/fr.json
- Migrate test-config module (name, config, descriptions) to i18n
- Both modules use module-level i18n/ directory with EN/FR locale files
- Module names in core i18n fallback: modules.thread-creator, modules.test-config
- Add i18n feature card to index.md
- Mention i18n in getting-started feature description, project structure, and naming conventions
- Add i18n init step to architecture boot sequence
- Add full Internationalisation section to creating-a-module.md
- Add Localisation section to commands.md, configuration.md, interactions.md, listeners.md
Use the config.currentValue translation key in the config.option
template so the 'Current: / Valeur actuelle :' label is shown again.

Also fix duplicate import in the test file.
…reak circular deps

Create src/core/utils/core-config.ts with getCoreT() and replyWithCoreT()
helpers that centralize the configService.getConfigForModuleIn(coreModule)
pattern. Update all config handlers and button interaction handlers to use
these helpers instead of importing coreModule directly, eliminating the
circular dependency chain (core.module -> handlers -> core.module) and
removing the repeated 4-line boilerplate from 9 files.
- type.text: Short text/Texte court -> Text/Texte
- type.boolean: Yes/No/Oui/Non -> Boolean/Booléen
- type.enum -> type.choice (actual key used by getConfigTypeName)
- config.toggle.enable/disable -> modules.enable/disable (actual keys)
- Update getConfigTypeName example to show correct values
- Fix stale config.toggle.enable references in configuration
  and creating-a-module guides
@RedsTom
RedsTom requested a review from AntoineJT June 17, 2026 17:23
@RedsTom RedsTom self-assigned this Jun 17, 2026
AntoineJT and others added 7 commits June 17, 2026 20:55
Stop persisting schema defaults; ConfigProvider.get() resolves them at
read time, looking up `config.<key>.default` for free-text fields so an
unconfigured value follows the guild locale. Enum/number/boolean/list
defaults are returned verbatim. thread-creator's welcome message uses
this; existing guilds keep their stored default (no migration).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Show a localized `_(default)_` suffix next to any value still served by
its schema default, so admins can tell what they have actually set apart
from untouched defaults. Backed by ConfigProvider.isDefault(key), which
the lazy-default resolution now makes possible (a stored value, including
a `null` clear, counts as explicitly set).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an optional `display: "language"` hint on enum entries; formatConfigValue
renders such options through Intl.DisplayNames in the viewer's locale. The core
`locale` field now shows Anglais/Français (FR) or English/French (EN) — both in
the /config readout and the select menu — with no translation keys to maintain.
Other enums and value types are returned verbatim as before.

The locale field description also drops its now-redundant "(en/fr)" suffix,
since the localized option names already convey the available languages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each `display: "language"` option now shows its flag in the select menu via
configValueEmoji, backed by a small curated code→flag table (en → 🇬🇧, since
English has no canonical flag). Codes absent from the table get no emoji.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A "Reset…" button at the bottom of the /config panel opens an ephemeral
multi-select listing the currently-overridden fields, plus an "All fields"
entry; the chosen fields are cleared and fall back to their defaults. The
button is disabled when nothing is overridden, so it is never a no-op.
Resetting removes the stored value (rather than writing null), letting the
lazy per-locale default take over again.

Adds ConfigProvider.isSet, ConfigService.resetFieldsForModuleIn, and a
page-aware source-panel refresh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"Core Module" / "Module Principal" was developer-facing jargon; the module a
guild admin actually opens via /config is the bot's global settings. Rename it
to "Global Configuration" (EN) / "Configuration Globale" (FR), in the schema
fallback and both locale bundles.

Also reword the config panel header to "Settings · {{moduleName}}" /
"Paramètres · {{moduleName}}", avoiding the redundant "Settings of Global
Configuration" the previous "of"-style template produced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The /config "default" marker keyed on isDefault, which was true only for an
unset field that *declares* a defaultValue — so an unset list with no default
(e.g. thread-creator's channels) showed "—" with no marker. Key it on !isSet
instead: any field the admin hasn't set is flagged. This also makes the marker
the exact complement of the reset picker (set ⟺ resettable ⟺ no marker). The
now-unused isDefault is removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AntoineJT AntoineJT changed the title feat: add i18n support with per-guild locale selection feat: i18n support and localized configuration UX Jun 17, 2026
@AntoineJT
AntoineJT enabled auto-merge (rebase) June 17, 2026 20:30
@AntoineJT
AntoineJT merged commit 1340edd into master Jun 17, 2026
7 of 8 checks passed
@AntoineJT
AntoineJT deleted the feat/i18n branch June 17, 2026 20:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants