Skip to content

Commit

Permalink
Add a method to register custom message providers
Browse files Browse the repository at this point in the history
  • Loading branch information
utarwyn committed Aug 5, 2023
1 parent a614a7e commit 2ce50f1
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 13 deletions.
13 changes: 13 additions & 0 deletions src/__tests__/I18nProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,17 @@ describe('I18nProvider', () => {
expect(provider.__(key, replacements)).toBe(expected);
}
);

describe('Dynamic message providers', () => {
it('should register and use a message provider of an existing key', () => {
provider['localeData'] = { 'game.load': 'default message' };
const message = 'Another game loading message';
provider.addProvider('game.load', () => message);
expect(provider.__('game.load')).toBe(message);
});

it('should throw an error if the key does not exist', () => {
expect(() => provider.addProvider('unknown_key', () => 'hello world')).toThrow();
});
});
});
7 changes: 7 additions & 0 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,11 @@ describe('TicTacToe', () => {
expect(eventHandler.registerListener).toHaveBeenCalledTimes(1);
expect(eventHandler.registerListener).toHaveBeenCalledWith('tie', listener);
});

test('should call addProvider from localize', () => {
const messageProvider = () => 'my message';
tictactoe.addMessageProvider('game.load', messageProvider);
expect(localize.addProvider).toHaveBeenCalledTimes(1);
expect(localize.addProvider).toHaveBeenCalledWith('game.load', messageProvider);
});
});
47 changes: 36 additions & 11 deletions src/i18n/I18nProvider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import fs from 'fs';
import path from 'path';

/**
* Represents a collection of replacements when translating a text.
*/
export type Replacements = { [key: string]: string | number | string[] };

/**
* Represents a message provider added programmatically.
*/
export type MessageProvider = () => string;

/**
* Default implementation to translate messages.
* Can load messages from integrated or external locale files.
Expand All @@ -20,10 +30,17 @@ export class I18nProvider {

/**
* Collection with paths of integrated locales in the module
* @private
*/
private readonly availableLocales: Map<string, string>;
/**
* Collection of message providers added programmatically
* @private
*/
private availableLocales: Map<string, string>;
private readonly messageProviders: Map<string, MessageProvider>;
/**
* Collection of all locale messages loaded from a language file
* @private
*/
private localeData?: Record<string, string>;

Expand All @@ -36,14 +53,15 @@ export class I18nProvider {
.readdirSync(localesPath)
.map(file => [path.basename(file, '.json'), path.resolve(localesPath, file)])
);
this.messageProviders = new Map<string, MessageProvider>();
}

/**
* Loads module messages from an internal or external file.
*
* @param locale locale key or language file to load
*/
loadFromLocale(locale?: string): void {
public loadFromLocale(locale?: string): void {
let filepath = this.availableLocales.get(locale ?? I18nProvider.DEFAULT_LOCALE);
let loaded = filepath !== undefined;

Expand Down Expand Up @@ -79,9 +97,9 @@ export class I18nProvider {
* @param replacements collection of replacement to operate on the message
* @returns translated message using replacements
*/
__(key: string, replacements?: Replacements): string {
public __(key: string, replacements?: Replacements): string {
if (this.localeData && this.localeData[key]) {
let message = this.localeData[key];
let message = this.messageProviders.get(key)?.() ?? this.localeData[key];

if (replacements) {
Object.entries(replacements).forEach(replacement => {
Expand All @@ -95,6 +113,20 @@ export class I18nProvider {
}
}

/**
* Adds a message provider for a given key.
* Key must already exist in the cache, otherwise an error will be thrown.
*
* @param key key corresponding to the added provider
* @param provider function that dynamically supplies the message
*/
public addProvider(key: string, provider: MessageProvider): void {
if (this.localeData?.[key] == null) {
throw new Error(`Cannot register message provider because key "${key}" does not exist`);
}
this.messageProviders.set(key, provider);
}

private static flatten<T extends Record<string, any>>(
object: T,
objectPath: string | null = null,
Expand All @@ -108,10 +140,3 @@ export class I18nProvider {
}, {} as T);
}
}

/**
* Allows to define some replacements when translating a text.
*/
export interface Replacements {
[key: string]: string | number | string[];
}
6 changes: 4 additions & 2 deletions src/i18n/localize.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { I18nProvider, Replacements } from '@i18n/I18nProvider';
import { I18nProvider, MessageProvider, Replacements } from '@i18n/I18nProvider';

const provider = new I18nProvider();

const loadFromLocale = (locale?: string): void => provider.loadFromLocale(locale);
const __ = (id: string, replacements?: Replacements): string => provider.__(id, replacements);
const addProvider = (id: string, messageProvider: MessageProvider): void =>
provider.addProvider(id, messageProvider);

export default { loadFromLocale, __ };
export default { loadFromLocale, __, addProvider };
12 changes: 12 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import TicTacToeBot from '@bot/TicTacToeBot';
import Config from '@config/Config';
import localize from '@i18n/localize';
import { Client, CommandInteraction, Intents, Message } from 'discord.js';
import { MessageProvider } from '@i18n/I18nProvider';

/**
* Controls all interactions between modules of the bot.
Expand Down Expand Up @@ -103,6 +104,17 @@ class TicTacToe {
): void {
this.eventHandler.registerListener(eventName, listener);
}

/**
* Adds a message provider for a given key.
* Key must exist, otherwise an error will be thrown.
*
* @param key key corresponding to the added provider
* @param provider function that dynamically supplies the message
*/
public addMessageProvider(key: string, provider: MessageProvider): void {
localize.addProvider(key, provider);
}
}

export = TicTacToe;

0 comments on commit 2ce50f1

Please sign in to comment.