feat: locales #27

Merged
Anri merged 26 commits from lang into main 2022-07-22 11:46:48 +02:00
7 changed files with 61 additions and 31 deletions
Showing only changes of commit 47da79eefc - Show all commits

View file

@ -3,7 +3,7 @@ import { Routes } from 'discord-api-types/v9';
import { Client } from 'discord.js'; import { Client } from 'discord.js';
import { readdir } from 'fs/promises'; import { readdir } from 'fs/promises';
/** Load all the commands */ /** Load all the commands. */
export default async (client: Client) => { export default async (client: Client) => {
const rest = new REST({ version: '9' }).setToken(client.token ?? ''); const rest = new REST({ version: '9' }).setToken(client.token ?? '');

View file

@ -7,7 +7,7 @@ export default {
data: (client: Client) => { data: (client: Client) => {
const filename = getFilename(__filename); const filename = getFilename(__filename);
return new SlashCommandBuilder() return new SlashCommandBuilder()
.setName(filename) .setName(filename.toLowerCase())
.setDescription(client.locales.get(client.config.default_lang)?.get(`c_${filename}_desc`) ?? '?') .setDescription(client.locales.get(client.config.default_lang)?.get(`c_${filename}_desc`) ?? '?')
.setNameLocalizations(getLocalizations(client, `c_${filename}_name`)) .setNameLocalizations(getLocalizations(client, `c_${filename}_name`))
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_desc`)); .setDescriptionLocalizations(getLocalizations(client, `c_${filename}_desc`));

View file

@ -1,7 +1,7 @@
import { Client } from 'discord.js'; import { Client } from 'discord.js';
import { readdir } from 'fs/promises'; import { readdir } from 'fs/promises';
/** Load all the events */ /** Load all the events. */
export default async (client: Client) => { export default async (client: Client) => {
const events_categories = (await readdir(__dirname)) const events_categories = (await readdir(__dirname))
.filter(element => !element.endsWith('.js') && !element.endsWith('.ts')); .filter(element => !element.endsWith('.js') && !element.endsWith('.ts'));

View file

@ -4,7 +4,7 @@ import loadCommands from './commands/loader';
import { logStart } from './utils/misc'; import { logStart } from './utils/misc';
/** Run the bot */ /** Run the bot. */
const run = async () => { const run = async () => {
console.log('Starting Botanique...'); console.log('Starting Botanique...');

View file

@ -3,15 +3,16 @@ import { readFileSync } from 'fs';
import { SlashCommandBuilder } from '@discordjs/builders'; import { SlashCommandBuilder } from '@discordjs/builders';
import { loadLocales } from './locales'; import { loadLocales } from './locales';
const { version } = JSON.parse(readFileSync('./package.json').toString());
declare module 'discord.js' { declare module 'discord.js' {
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
export interface Client { export interface Client {
/** Store the configuration */ /** Store the configuration */
config: { config: {
/** Bot version */
version: string, version: string,
/** Bot token from env variable */
token_discord: string | undefined, token_discord: string | undefined,
/** Default lang used */
default_lang: string default_lang: string
}, },
/** Store all the slash commands */ /** Store all the slash commands */
@ -27,7 +28,7 @@ declare module 'discord.js' {
} }
} }
/** Creation of the client and definition of its properties */ /** Creation of the client and definition of its properties. */
export default async () => { export default async () => {
const client: Client = new Client({ const client: Client = new Client({
intents: [ intents: [
@ -36,14 +37,14 @@ export default async () => {
}); });
client.config = { client.config = {
version: version, version: JSON.parse(readFileSync('./package.json').toString()).version,
token_discord: process.env.TOKEN_DISCORD, token_discord: process.env.TOKEN_DISCORD,
default_lang: 'en-US', default_lang: 'en-US',
}; };
client.commands = new Collection(); client.commands = new Collection();
console.log('Localizations progression :'); console.log('Translations progression :');
client.locales = await loadLocales(client.config.default_lang); client.locales = await loadLocales(client.config.default_lang);
return client; return client;

View file

@ -3,33 +3,38 @@ import { readdir } from 'fs/promises';
import { removeExtension } from './misc'; import { removeExtension } from './misc';
/** /**
* Load the localizations files into memory * Load the localizations files into memory.
*
* Show percentage of translations.
* @param default_lang default lang * @param default_lang default lang
* @returns Map of map with all the localizations * @returns Map of map with all the localizations
*/ */
export const loadLocales = async (default_lang: string) => { export const loadLocales = async (default_lang: string) => {
// Get files from locales/ directory
const old_path = __dirname.split('/'); const old_path = __dirname.split('/');
old_path.pop(); old_path.pop();
const files = await readdir(`${old_path.join('/')}/locales`); const files = await readdir(`${old_path.join('/')}/locales`);
// Read JSON files content and load it into the memory
const locales = new Map<string, Map<string, string>>(); const locales = new Map<string, Map<string, string>>();
await Promise.all( await Promise.all(
files.map(async lang => { files.map(async lang => {
// Import file
const content: { const content: {
[key: string]: string [key: string]: string
} = await import( } = await import(
`../locales/${lang}` `../locales/${lang}`
); );
// Add it to the memory
locales.set( locales.set(
removeExtension(lang), removeExtension(lang),
new Map( new Map(Object.keys(content)
Object.keys(content) // Ignore the default key
.filter(str => str !== 'default') .filter(str => str !== 'default')
.map(str => { .map(str => {
return [str, content[str]]; return [str, content[str]];
}), }),
) )
); );
}) })
@ -43,7 +48,7 @@ export const loadLocales = async (default_lang: string) => {
/** /**
* Builds a dictionary, if a translation is not available, * Builds a dictionary, if a translation is not available,
* we fallback to en-US. * we fallback to default lang.
* @param client Client * @param client Client
* @param text Name of string to fetch * @param text Name of string to fetch
* @returns the dictionary * @returns the dictionary
@ -51,12 +56,16 @@ export const loadLocales = async (default_lang: string) => {
export const getLocalizations = (client: Client, text: string) => { export const getLocalizations = (client: Client, text: string) => {
const data: Record<string, string> = {}; const data: Record<string, string> = {};
// Load all the localizations
client.locales.forEach((locale, lang) => { client.locales.forEach((locale, lang) => {
// Fetch the text and fallback to default lang if needed
// See getLocale for more info on why we *can* fallback
const str = locale.get(text) const str = locale.get(text)
?? client.locales.get(client.config.default_lang)?.get(text); ?? client.locales.get(client.config.default_lang)?.get(text);
// Store it if defined
if (str !== undefined) { if (str !== undefined) {
data[lang] = str.toLowerCase(); data[lang] = str;
} }
}); });
@ -65,15 +74,22 @@ export const getLocalizations = (client: Client, text: string) => {
/** /**
* Return the locale data for a lang, * Return the locale data for a lang,
* fallback to default language when a string isn't available * fallback to default language when a string isn't available.
* @param client Client * @param client Client
* @param lang Lang to fetch * @param lang Lang to fetch
* @returns the map with the desired languaged clogged with the default one * @returns the map with the desired languaged clogged with the default one
*/ */
export const getLocale = (client: Client, lang: string) => { export const getLocale = (client: Client, lang: string) => {
// Load default lang
const default_locales = client.locales.get(client.config.default_lang); const default_locales = client.locales.get(client.config.default_lang);
// Load desired lang
const desired_locales = client.locales.get(lang); const desired_locales = client.locales.get(lang);
// Get text and fallback to default lang if needed
//
// We can fallback to the default one without any problem
// because we make sure that the default language always contains
// the desired text, and that the other languages are only translations
const locales = new Map(); const locales = new Map();
default_locales?.forEach((_, key) => { default_locales?.forEach((_, key) => {
locales.set(key, desired_locales?.get(key) ?? default_locales.get(key)); locales.set(key, desired_locales?.get(key) ?? default_locales.get(key));
@ -83,15 +99,15 @@ export const getLocale = (client: Client, lang: string) => {
}; };
/** /**
* Show percentage of translation progression * Show percentage of translation progression.
* *
* Raise an error if the default lang isn't * Raise an error if the default lang isn't
* the lang with most text * the lang with most text.
* @param locales Locales loaded * @param locales Locales loaded
* @param default_lang default lang * @param default_lang default lang
* @returns void * @returns void
*/ */
export const checkLocales = const checkLocales =
async (locales: Map<string, Map<string, string>>, default_lang: string) => { async (locales: Map<string, Map<string, string>>, default_lang: string) => {
// Associate each lang with the number of locale it has // Associate each lang with the number of locale it has
let locales_size = new Map<string, number>(); let locales_size = new Map<string, number>();
@ -108,6 +124,10 @@ async (locales: Map<string, Map<string, string>>, default_lang: string) => {
const [max_size] = locales_size.values(); const [max_size] = locales_size.values();
const default_lang_size = locales_size.get(default_lang) ?? 0; const default_lang_size = locales_size.get(default_lang) ?? 0;
if (max_size > default_lang_size) { if (max_size > default_lang_size) {
// Throw error because in this case we are sure than the security
// explained in getLocale isn't true.
// However, it is possible that this condition is true
// and the security is poor, but it's better than nothing.
throw new Error( throw new Error(
`The default locale (${default_lang} = ${default_lang_size}) isn't complete ` `The default locale (${default_lang} = ${default_lang_size}) isn't complete `
+ `(${max_size_name} = ${max_size}).` + `(${max_size_name} = ${max_size}).`
@ -122,26 +142,29 @@ async (locales: Map<string, Map<string, string>>, default_lang: string) => {
const bar_size = 4; const bar_size = 4;
locales_size.forEach((size, lang) => { locales_size.forEach((size, lang) => {
const percentage = (size / max_size) * 100; const percentage = (size / max_size) * 100;
// Colored bar part
const blocks = ' '.repeat(percentage / bar_size); const blocks = ' '.repeat(percentage / bar_size);
// Blank bar part
const blank = ' '.repeat((100 - percentage) / bar_size); const blank = ' '.repeat((100 - percentage) / bar_size);
const color = () => { const color = () => {
switch (true) { switch (true) {
case percentage <= 25: case percentage <= 25:
// red // Red
return '\x1b[41m'; return '\x1b[41m';
case percentage <= 50: case percentage <= 50:
// mangeta // Mangeta
return '\x1b[45m'; return '\x1b[45m';
case percentage <= 75: case percentage <= 75:
// cyan // Cyan
return '\x1b[46m'; return '\x1b[46m';
case percentage <= 100: case percentage <= 100:
// green // Green
return '\x1b[42m'; return '\x1b[42m';
default: default:
return ''; return '';
} }
}; };
console.log(`${lang} | ${color()}${blocks}\x1b[0m${blank} | ${percentage}%`); console.log(`${lang} | ${color()}${blocks}\x1b[0m${blank} | ${percentage}%`);
}); });
}; };

View file

@ -1,5 +1,5 @@
/** /**
* Log module status * Log module status.
* @param {string} name Module name * @param {string} name Module name
* @param {boolean} status Module status * @param {boolean} status Module status
* @returns String * @returns String
@ -9,18 +9,24 @@ export const logStart = (name: string, status: boolean) => {
}; };
/** /**
* Filename without path and extension * Filename without path and extension.
* @param path __filename * @param path __filename
* @returns string * @returns string
*/ */
export const getFilename = (path: string) => { export const getFilename = (path: string) => {
const path_list = path.split('/'); const path_list = path.split('/');
return path_list[path_list.length - 1].split('.')[0]; // Check if filename exist
const filename_with_ext = path_list.pop();
if (filename_with_ext === undefined) {
throw new Error(`Filename error: don't exist in ${path}`);
}
return removeExtension(filename_with_ext);
}; };
/** /**
* Remove extension from a filename * Remove extension from a filename.
* @param filename string of the filename with an extension * @param filename string of the filename with an extension
* @returns string of the filename without an extension * @returns string of the filename without an extension
*/ */