🌐 feat(locales): add localization strings #49
Conversation
- Introduced various error messages and notifications - Enhanced user feedback for permissions and bot status
- Adds `ky` for making HTTP requests with improved syntax. - Adds `yaml` for parsing YAML configuration files. - Refactors member fetching to use `ky` for improved readability and conciseness.
- Introduces a utility function to retrieve localization files based on the specified language. - Supports French and defaults to English localization.
- Replaces double quotes with single quotes in the English locale file for consistency. - Corrects the file path for locale files and updates the type definition.
- Implements internationalization (i18n) for bot responses. - Uses YAML files to store translations. - Retrieves server language and uses it to display messages. - Adds `en.yml` with default messages.
- Removes an unused import to improve code cleanliness. - Eliminates `parse` from `yaml` as it is no longer needed.
- Adds the `nodejs_compat` compatibility flag to the Wrangler configuration. - Updates the generated types to include NodeJS process environment variables. - This allows the worker to access NodeJS-compatible APIs.
There was a problem hiding this comment.
Pull request overview
This PR adds internationalization (i18n) support to enable Discord bot commands to respond in the server's preferred language. The implementation includes language detection from Discord guilds and YAML-based localization files for English and French.
Key changes:
- Added utility functions to fetch server language from Discord API and load localization files
- Created YAML-based localization files for English and French translations
- Integrated localization into command handlers for
/help,/someone, and/pingcommands
Reviewed changes
Copilot reviewed 8 out of 10 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| wrangler.jsonc | Added nodejs_compat compatibility flag to enable Node.js APIs |
| worker-configuration.d.ts | Auto-generated type definitions updated to reflect the new compatibility flag |
| src/utils/getLanguageFromServer.ts | New utility to fetch guild language preference from Discord API |
| src/utils/getFileFromLanguage.ts | New utility to load YAML localization files based on language code |
| src/types/fileLanguage.ts | TypeScript interface defining the structure of localization strings |
| src/locales/fr.yml | French translations for all bot messages and errors |
| src/locales/en.yml | English translations for all bot messages and errors |
| src/index.ts | Updated command handlers to load and use localized strings |
| package.json | Added ky and yaml dependencies for HTTP requests and YAML parsing |
| package-lock.json | Lockfile updated with new dependency entries |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import fs from 'fs'; | ||
| import path from 'path'; | ||
| import type { FileLanguage } from '../types/fileLanguage'; | ||
|
|
||
| export default function getFileFromLanguage(language: string): unknown { | ||
| let fileName: string; | ||
| switch (language) { | ||
| case 'fr': | ||
| fileName = 'fr.yml'; | ||
| break; | ||
| default: | ||
| fileName = 'en.yml'; | ||
| } | ||
| const filePath = path.resolve(__dirname, 'locales', fileName); | ||
| const fileContents = fs.readFileSync(filePath, 'utf8'); |
There was a problem hiding this comment.
Using Node.js fs and path modules in a Cloudflare Workers environment will not work. Cloudflare Workers don't have access to the file system. You need to either bundle the YAML files as static assets or import them directly. Consider using Wrangler's asset binding or importing the YAML files at build time instead of reading them with fs.readFileSync at runtime.
| import fs from 'fs'; | |
| import path from 'path'; | |
| import type { FileLanguage } from '../types/fileLanguage'; | |
| export default function getFileFromLanguage(language: string): unknown { | |
| let fileName: string; | |
| switch (language) { | |
| case 'fr': | |
| fileName = 'fr.yml'; | |
| break; | |
| default: | |
| fileName = 'en.yml'; | |
| } | |
| const filePath = path.resolve(__dirname, 'locales', fileName); | |
| const fileContents = fs.readFileSync(filePath, 'utf8'); | |
| import type { FileLanguage } from '../types/fileLanguage'; | |
| import enYaml from '../locales/en.yml'; | |
| import frYaml from '../locales/fr.yml'; | |
| export default function getFileFromLanguage(language: string): unknown { | |
| let fileContents: string; | |
| switch (language) { | |
| case 'fr': | |
| fileContents = frYaml; | |
| break; | |
| default: | |
| fileContents = enYaml; | |
| } |
| // get guild id | ||
| const guildId = c.interaction.guild?.id; | ||
| if (!guildId) return c.res('<a:crossmark:1454281378295451648> **Guild not found.**'); | ||
| if (!guildId) return c.res(lang.GUILD_NOT_FOUND); |
There was a problem hiding this comment.
The guildId is checked and returned early on line 22, making this duplicate check on line 35 redundant and unreachable. The code comment "get guild id" is also misleading since the guildId was already retrieved earlier. Remove this redundant check.
| const guildId = c.interaction.guild?.id; | ||
| const memberPermissions = c.interaction.member?.permissions; | ||
| if (!memberPermissions) return c.res('<a:crossmark:1454281378295451648> **Unable to verify permissions.**'); | ||
| if (!guildId) return c.res('<a:crossmark:1454281378295451648> **Guild not found.**'); |
There was a problem hiding this comment.
The language loading happens after the first guild check. If guildId is null at line 22, the function returns before the language is loaded. Move the guildId null check after the language loading, or handle the error message using a default language. Currently, line 22 returns a hardcoded English error message even if the server uses French.
| let lang: FileLanguage = getFileFromLanguage('en') as FileLanguage; | ||
| const guildId = c.interaction.guild?.id as string; | ||
| await getLanguageFromServer(guildId, c).then((language) => { | ||
| lang = getFileFromLanguage(language) as FileLanguage; | ||
| }); |
There was a problem hiding this comment.
The language loading pattern is duplicated in three command handlers. Consider extracting this logic into a helper function or middleware to avoid code duplication and ensure consistent language loading behavior across all commands.
| await getLanguageFromServer(guildId, c).then((language) => { | ||
| lang = getFileFromLanguage(language) as FileLanguage; | ||
| }); |
There was a problem hiding this comment.
Using .then() is unnecessary here since the code already uses await. You can simplify this to: const language = await getLanguageFromServer(guildId, c); lang = getFileFromLanguage(language) as FileLanguage;. This makes the code more readable and consistent with the async/await pattern used elsewhere.
| searchParams: { limit: '1000' }, | ||
| headers: { Authorization: `Bot ${token}` }, | ||
| }); | ||
| if (!resp.ok) return c.res(`Failed to fetch members: ${resp.status} ${resp.statusText}`); |
There was a problem hiding this comment.
Error responses should also be localized. This hardcoded error message "Failed to fetch members: {status} {statusText}" will always be in English, even if the server uses French. Consider adding this error message to the localization files.
| if (!resp.ok) return c.res(`Failed to fetch members: ${resp.status} ${resp.statusText}`); | |
| if (!resp.ok) return c.res(`${resp.status} ${resp.statusText}`); |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Warning
This PR is still an W.I.P