From 7331038cb05d0930bd3a2a8e15945b99b9d78407 Mon Sep 17 00:00:00 2001 From: jben7 <42550761+jben7@users.noreply.github.com> Date: Sun, 11 Feb 2024 00:01:11 +0100 Subject: [PATCH] feat(locales): Add translations support --- locales/en/translation.json | 32 +++++++++++++++ locales/fr/translation.json | 32 +++++++++++++++ package-lock.json | 77 +++++++++++++++++++++++++++++++++++-- package.json | 2 + src/i18next.js | 21 ++++++++++ src/index.js | 25 ++++++------ src/main.js | 28 ++++++++------ 7 files changed, 192 insertions(+), 25 deletions(-) create mode 100644 locales/en/translation.json create mode 100644 locales/fr/translation.json create mode 100644 src/i18next.js diff --git a/locales/en/translation.json b/locales/en/translation.json new file mode 100644 index 000000000..eae075f81 --- /dev/null +++ b/locales/en/translation.json @@ -0,0 +1,32 @@ +{ + "errors": { + "unsupported_nodejs_version": "Unsupported NodeJS version! Please install Node.js 12, 13, or 14.", + "unknown_error": "Unknown error occurred" + }, + "messages": { + "starting_modmail": "Starting Modmail", + "on_nodejs": "on Node.js", + "please_run_npm_ci": "Please run \"npm ci\" before starting the bot", + "error_disallowed_intents": "Error: Disallowed intents specified", + "to_run_the_bot": "To run the bot, you must enable 'Server Members Intent' on your bot's page in the Discord Developer Portal:", + "go_to_discord_developers": "Go to https://discord.com/developers/applications", + "click_on_your_bot": "Click on your bot", + "click_bot_sidebar": "Click 'Bot' on the sidebar", + "turn_on_server_members_intent": "Turn on 'Server Members Intent'", + "please_run_npm_ci_again": "Please run \"npm ci\" again! Package \"{{testedPackage}}\" is missing.", + "is_missing": "is missing.", + "updating_database": "Updating database. This can take a while. Don't close the bot!", + "done": "Done!", + "preparing_plugins": "Preparing plugins...", + "connecting_to_discord": "Connecting to Discord...", + "connected_waiting_servers": "Connected! Waiting for servers to become available...", + "servers_not_available": "Servers did not become available after 15 seconds, continuing start-up anyway", + "single_server_warning": "WARNING: The bot will not work before it's invited to the server.", + "multiple_servers_warning": "WARNING: The bot will not function correctly until it's invited to *all* main servers and the inbox server.", + "both_servers_warning": "WARNING: The bot will not function correctly until it's invited to *both* the main server and the inbox server.", + "initializing": "Initializing...", + "loading_plugins": "Loading plugins...", + "loaded_plugins": "Loaded {{loadedCount}} plugins ({{baseCount}} built-in plugins, {{externalCount}} external plugins)", + "listening_to_dms": "Done! Now listening to DMs." + } +} diff --git a/locales/fr/translation.json b/locales/fr/translation.json new file mode 100644 index 000000000..efc804187 --- /dev/null +++ b/locales/fr/translation.json @@ -0,0 +1,32 @@ +{ + "errors": { + "unsupported_nodejs_version": "Version de NodeJS non supportée ! Veuillez installer Node.js 12, 13 ou 14.", + "unknown_error": "Une erreur inconnue s'est produite" + }, + "messages": { + "starting_modmail": "Démarrage de Modmail", + "on_nodejs": "sur Node.js", + "please_run_npm_ci": "Veuillez exécuter \"npm ci\" avant de démarrer le bot", + "error_disallowed_intents": "Erreur : Intentions interdites spécifiées", + "to_run_the_bot": "Pour exécuter le bot, vous devez activer 'Server Members Intent' sur la page de votre bot dans le portail développeur Discord :", + "go_to_discord_developers": "Allez sur https://discord.com/developers/applications", + "click_on_your_bot": "Cliquez sur votre bot", + "click_bot_sidebar": "Cliquez sur 'Bot' dans la barre latérale", + "turn_on_server_members_intent": "Activez 'Server Members Intent'", + "please_run_npm_ci_again": "Veuillez exécuter \"npm ci\" à nouveau ! Le package \"{{testedPackage}}\" est manquant.", + "is_missing": "est manquant.", + "updating_database": "Mise à jour de la base de données. Cela peut prendre un certain temps. Ne fermez pas le bot !", + "done": "Terminé !", + "preparing_plugins": "Préparation des plugins...", + "connecting_to_discord": "Connexion à Discord...", + "connected_waiting_servers": "Connecté ! Attente de la disponibilité des serveurs...", + "servers_not_available": "Les serveurs n'ont pas été disponibles après 15 secondes, poursuite du démarrage quand même", + "single_server_warning": "AVERTISSEMENT : Le bot ne fonctionnera pas avant d'être invité sur le serveur.", + "multiple_servers_warning": "AVERTISSEMENT : Le bot ne fonctionnera pas correctement tant qu'il ne sera pas invité à *tous* les serveurs principaux et le serveur de la boîte de réception.", + "both_servers_warning": "AVERTISSEMENT : Le bot ne fonctionnera pas correctement tant qu'il ne sera pas invité à *à la fois* le serveur principal et le serveur de la boîte de réception.", + "initializing": "Initialisation...", + "loading_plugins": "Chargement des plugins...", + "loaded_plugins": "Chargé {{loadedCount}} plugins ({{baseCount}} plugins intégrés, {{externalCount}} plugins externes)", + "listening_to_dms": "Terminé ! Maintenant à l'écoute des MP." + } +} diff --git a/package-lock.json b/package-lock.json index de97ada3d..7c18b42b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "modmailbot", - "version": "3.6.1", + "version": "3.7.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "modmailbot", - "version": "3.6.1", + "version": "3.7.0", "license": "MIT", "dependencies": { "ajv": "^8.12.0", @@ -16,6 +16,8 @@ "express": "^4.18.2", "helmet": "^7.0.0", "humanize-duration": "^3.29.0", + "i18next": "^23.8.2", + "i18next-fs-backend": "^2.3.1", "ini": "^4.1.1", "json5": "^2.2.3", "knex": "^2.5.1", @@ -39,7 +41,7 @@ "nodemon": "^3.0.1" }, "engines": { - "node": ">=18.0.0 <20.0.0" + "node": ">=16.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -63,6 +65,17 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -2846,6 +2859,33 @@ "ms": "^2.0.0" } }, + "node_modules/i18next": { + "version": "23.8.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.8.2.tgz", + "integrity": "sha512-Z84zyEangrlERm0ZugVy4bIt485e/H8VecGUZkZWrH7BDePG6jT73QdL9EA1tRTTVVMpry/MgWIP1FjEn0DRXA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-fs-backend": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.3.1.tgz", + "integrity": "sha512-tvfXskmG/9o+TJ5Fxu54sSO5OkY6d+uMn+K6JiUGLJrwxAVfer+8V3nU8jq3ts9Pe5lXJv4b1N7foIjJ8Iy2Gg==" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -5074,6 +5114,11 @@ "node": ">=0.10.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6253,6 +6298,14 @@ "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", "dev": true }, + "@babel/runtime": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, "@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -8373,6 +8426,19 @@ "ms": "^2.0.0" } }, + "i18next": { + "version": "23.8.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.8.2.tgz", + "integrity": "sha512-Z84zyEangrlERm0ZugVy4bIt485e/H8VecGUZkZWrH7BDePG6jT73QdL9EA1tRTTVVMpry/MgWIP1FjEn0DRXA==", + "requires": { + "@babel/runtime": "^7.23.2" + } + }, + "i18next-fs-backend": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.3.1.tgz", + "integrity": "sha512-tvfXskmG/9o+TJ5Fxu54sSO5OkY6d+uMn+K6JiUGLJrwxAVfer+8V3nU8jq3ts9Pe5lXJv4b1N7foIjJ8Iy2Gg==" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -9997,6 +10063,11 @@ } } }, + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/package.json b/package.json index a4c50d8bb..5719b02b8 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "express": "^4.18.2", "helmet": "^7.0.0", "humanize-duration": "^3.29.0", + "i18next": "^23.8.2", + "i18next-fs-backend": "^2.3.1", "ini": "^4.1.1", "json5": "^2.2.3", "knex": "^2.5.1", diff --git a/src/i18next.js b/src/i18next.js new file mode 100644 index 000000000..2b081e304 --- /dev/null +++ b/src/i18next.js @@ -0,0 +1,21 @@ +// Initialize i18next for translations +const fs = require("fs"); +const path = require("path"); +const i18next = require("i18next") +const Backend = require("i18next-fs-backend") + +i18next + .use(Backend) + .init({ + initImmediate: false, + lng: "fr", + fallbackLng: "en", + preload: ["en", "fr"], + ns: ["translation"], + defaultNS: "translation", + backend: { + loadPath: "locales/{{lng}}/{{ns}}.json" + } + }) + +module.exports = i18next; diff --git a/src/index.js b/src/index.js index d770800f3..cba103fef 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,10 @@ +//Init translations +const i18next = require("./i18next"); + // Verify NodeJS version const nodeMajorVersion = parseInt(process.versions.node.split(".")[0], 10); if (nodeMajorVersion < 12) { - console.error("Unsupported NodeJS version! Please install Node.js 12, 13, or 14."); + console.error(i18next.t("errors.unsupported_nodejs_version")); process.exit(1); } @@ -16,7 +19,7 @@ const path = require("path"); try { fs.accessSync(path.join(__dirname, "..", "node_modules")); } catch (e) { - console.error("Please run \"npm ci\" before starting the bot"); + console.error(i18next.t("messages.please_run_npm_ci")); process.exit(1); } @@ -41,14 +44,14 @@ function errorHandler(err) { // Leave out stack traces for BotErrors (the message has enough info) console.error(`Error: ${err.message}`); } else if (err.message === "Disallowed intents specified") { - let fullMessage = "Error: Disallowed intents specified"; + let fullMessage = i18next.t("messages.error_disallowed_intents"); fullMessage += "\n\n"; - fullMessage += "To run the bot, you must enable 'Server Members Intent' on your bot's page in the Discord Developer Portal:"; + fullMessage += i18next.t("messages.to_run_the_bot"); fullMessage += "\n\n"; - fullMessage += "1. Go to https://discord.com/developers/applications" - fullMessage += "2. Click on your bot" - fullMessage += "3. Click 'Bot' on the sidebar" - fullMessage += "4. Turn on 'Server Members Intent'" + fullMessage += i18next.t("messages.go_to_discord_developers"); + fullMessage += i18next.t("messages.click_on_your_bot"); + fullMessage += i18next.t("messages.click_bot_sidebar"); + fullMessage += i18next.t("messages.turn_on_server_members_intent"); console.error(fullMessage); } else if (err instanceof PluginInstallationError) { @@ -89,7 +92,7 @@ try { fs.accessSync(path.join(__dirname, "..", "node_modules", mod)) }); } catch (e) { - console.error(`Please run "npm ci" again! Package "${testedPackage}" is missing.`); + console.error(i18next.t("messages.please_run_npm_ci_again", { testedPackage })); process.exit(1); } @@ -101,9 +104,9 @@ try { // Make sure the database is up to date const [completed, newMigrations] = await knex.migrate.list(); if (newMigrations.length > 0) { - console.log("Updating database. This can take a while. Don't close the bot!"); + console.log(i18next.t("messages.updating_database")); await knex.migrate.latest(); - console.log("Done!"); + console.log(i18next.t("messages.done")); } // Start the bot diff --git a/src/main.js b/src/main.js index bce3385bc..0c212bf10 100644 --- a/src/main.js +++ b/src/main.js @@ -4,6 +4,7 @@ const path = require("path"); const config = require("./cfg"); const bot = require("./bot"); const knex = require("./knex"); +const i18next = require("./i18next"); const { messageQueue } = require("./queue"); const utils = require("./utils"); const { formatters } = require("./formatters") @@ -20,28 +21,28 @@ const {getOrFetchChannel} = require("./utils"); module.exports = { async start() { - console.log("Preparing plugins..."); + console.log(i18next.t("messages.preparing_plugins")); await installAllPlugins(); - console.log("Connecting to Discord..."); + console.log(i18next.t("messages.connecting_to_discord")); bot.once("ready", async () => { - console.log("Connected! Waiting for servers to become available..."); + console.log(i18next.t("messages.connected_waiting_servers")); await (new Promise(resolve => { const waitNoteTimeout = setTimeout(() => { - console.log("Servers did not become available after 15 seconds, continuing start-up anyway"); + console.log(i18next.t("messages.servers_not_available")); console.log(""); const isSingleServer = config.mainServerId.includes(config.inboxServerId); if (isSingleServer) { - console.log("WARNING: The bot will not work before it's invited to the server."); + console.log(i18next.t("messages.single_server_warning")); } else { const hasMultipleMainServers = config.mainServerId.length > 1; if (hasMultipleMainServers) { - console.log("WARNING: The bot will not function correctly until it's invited to *all* main servers and the inbox server."); + console.log(i18next.t("messages.multiple_servers_warning")); } else { - console.log("WARNING: The bot will not function correctly until it's invited to *both* the main server and the inbox server."); + console.log(i18next.t("messages.both_servers_warning")); } } @@ -59,17 +60,22 @@ module.exports = { }); })); - console.log("Initializing..."); + console.log(i18next.t("messages.initializing")); initStatus(); initBaseMessageHandlers(); initUpdateNotifications(); - console.log("Loading plugins..."); + console.log(i18next.t("messages.loading_plugins")); const pluginResult = await loadAllPlugins(); - console.log(`Loaded ${pluginResult.loadedCount} plugins (${pluginResult.baseCount} built-in plugins, ${pluginResult.externalCount} external plugins)`); + console.log(i18next.t("messages.loaded_plugins", { + loadedCount: pluginResult.loadedCount, + baseCount: pluginResult.baseCount, + externalCount: pluginResult.externalCount + } + )); console.log(""); - console.log("Done! Now listening to DMs."); + console.log(i18next.t("messages.listening_to_dms")); console.log(""); const openThreads = await threads.getAllOpenThreads();