Skip to content

Commit

Permalink
refactor(i18next)!: remove sendLocalized, replyLocalized, and `ed…
Browse files Browse the repository at this point in the history
…itLocalized` (#335)

BREAKING CHANGE: `sendLocalized`, `replyLocalized`, and `editLocalized` have been removed as these methods never properly supported all i18next and DiscordJS options and were extremely hard to maintain. The replacement is using either `fetchT` or `resolveKey` and implementing your own `message.channel.send` / `interaction.reply`
BREAKING CHANGE: `InternationalizationContext.author` has been removed as per the deprecation notice in the previous version. Please switch to `InternationalizationContext.user`.
  • Loading branch information
favna committed Aug 11, 2022
1 parent 1cfc32a commit e53558f
Show file tree
Hide file tree
Showing 2 changed files with 5 additions and 334 deletions.
309 changes: 3 additions & 306 deletions packages/i18next/src/lib/functions.ts
@@ -1,16 +1,8 @@
import { container } from '@sapphire/pieces';
import { cast, isObject, NonNullObject } from '@sapphire/utilities';
import type { NonNullObject } from '@sapphire/utilities';
import { BaseCommandInteraction, Guild, Message, MessageComponentInteraction } from 'discord.js';
import type { StringMap, TFunctionKeys, TFunctionResult, TOptions } from 'i18next';
import { deprecate } from 'node:util';
import type {
ChannelTarget,
InternationalizationContext,
LocalizedInteractionReplyOptions,
LocalizedMessageOptions,
Target,
TextBasedDiscordChannel
} from './types';
import type { InternationalizationContext, Target } from './types';

/**
* Retrieves the language name for a specific target, using {@link InternationalizationHandler.fetchLanguage}.
Expand Down Expand Up @@ -70,7 +62,7 @@ export async function fetchT(target: Target) {
* @since 2.0.0
* @param target The target to fetch the language key from.
* @param key The i18next key.
* @param values The values to be passed to TFunction.
* @param options The options to be passed to TFunction.
* @returns The data that `key` held, processed by i18next.
*/
export async function resolveKey<
Expand All @@ -81,305 +73,10 @@ export async function resolveKey<
return container.i18n.format(typeof options?.lng === 'string' ? options.lng : await fetchLanguage(target), key, options);
}

/**
* Send a localized message using the language `keys` from your i18next language setup.
* @since 2.0.0
* @param target The target to send the message to.
* @param keys The language keys to be sent.
* @example
* ```typescript
* // Using a string to specify the key to send
* await sendLocalized(message, 'commands/ping:loading');
* // ➡ "Pinging..."
* ```
*/
export async function sendLocalized<TKeys extends TFunctionKeys = string>(target: ChannelTarget, keys: TKeys | TKeys[]): Promise<Message>;
/**
* Send a localized message using {@link LocalizedMessageOptions}.
* @since 2.0.0
* @param target The target to send the message to.
* @param options A {@link LocalizedMessageOptions} object, requiring at least a `keys` field.
* @example
* ```typescript
* // Using an object to specify the key to send
* await sendLocalized(message, { keys: 'commands/ping:loading' });
* // ➡ "Pinging..."
* ```
* @example
* ```typescript
* // Passing interpolation options into i18next
* const latency = 42;
*
* await sendLocalized(message, {
* keys: 'commands/ping:loading',
* formatOptions: { latency }
* });
* // ➡ "Pinging... current latency is 42ms."
* ```
*/
export async function sendLocalized<TKeys extends TFunctionKeys = string, TInterpolationMap extends NonNullObject = StringMap>(
target: ChannelTarget,
options: LocalizedMessageOptions<TKeys, TInterpolationMap>
): Promise<Message>;
export async function sendLocalized<TKeys extends TFunctionKeys = string, TInterpolationMap extends NonNullObject = StringMap>(
target: ChannelTarget,
options: TKeys | TKeys[] | LocalizedMessageOptions<TKeys, TInterpolationMap>
): Promise<Message> {
const channel = resolveTextChannel(target);
return channel.send(await resolveOverloads(target, options));
}

/**
* Replies to a message using the language `keys` from your i18next language setup.
* @since 2.0.0
* @param target The message to reply to.
* @param keys The language keys to be sent.
* @example
* ```typescript
* // Using an object to specify the key to send
* await replyLocalized(message, 'commands/ping:loading');
* // ➡ "Pinging..."
* ```
*/
export async function replyLocalized<TKeys extends TFunctionKeys = string>(target: Message, keys: TKeys | TKeys[]): Promise<Message>;
/**
* Replies to a message using {@link LocalizedMessageOptions}.
* @since 2.0.0
* @param target The message to reply to.
* @param options A {@link LocalizedMessageOptions} object, requiring at least a `keys` field.
* @example
* ```typescript
* // Using an object to specify the key to send
* await replyLocalized(message, { keys: 'commands/ping:loading' });
* // ➡ "Pinging..."
* ```
* @example
* ```typescript
* // Passing interpolation options into i18next
* const latency = 42;
*
* await replyLocalized(message, {
* keys: 'commands/ping:loading',
* formatOptions: { latency }
* });
* // ➡ "Pinging... current latency is 42ms."
* ```
*/
export async function replyLocalized<TKeys extends TFunctionKeys = string, TInterpolationMap extends NonNullObject = StringMap>(
target: Message,
options: LocalizedMessageOptions<TKeys, TInterpolationMap>
): Promise<Message>;
/**
* Replies to the interaction using the language `keys` from your i18next language setup.
* @since 2.4.0
* @param target The interaction to reply to.
* @param keys The language keys to be sent.
* @example
* ```typescript
* // Using an object to specify the key to send
* await replyLocalized(interaction, 'commands/ping:loading');
* // ➡ "Pinging..."
* ```
*/
export async function replyLocalized<TKeys extends TFunctionKeys = string>(
target: BaseCommandInteraction | MessageComponentInteraction,
keys: TKeys | TKeys[]
): Promise<ReturnType<(BaseCommandInteraction | MessageComponentInteraction)['reply']>>;
/**
* Replies to the interaction using {@link LocalizedInteractionReplyOptions}.
* @since 2.4.0
* @param target The interaction to reply to.
* @param options A {@link LocalizedInteractionReplyOptions} object, requiring at least a `keys` field.
* @example
* ```typescript
* // Using an object to specify the key to send
* await replyLocalized(interaction, { keys: 'commands/ping:loading' });
* // ➡ "Pinging..."
* ```
* @example
* ```typescript
* // Passing interpolation options into i18next
* const latency = 42;
*
* await replyLocalized(interaction, {
* keys: 'commands/ping:loading',
* formatOptions: { latency }
* });
* // ➡ "Pinging... current latency is 42ms."
* ```
*/
export async function replyLocalized<TKeys extends TFunctionKeys = string, TInterpolationMap extends NonNullObject = StringMap>(
target: BaseCommandInteraction | MessageComponentInteraction,
options: LocalizedInteractionReplyOptions<TKeys, TInterpolationMap>
): Promise<ReturnType<(BaseCommandInteraction | MessageComponentInteraction)['reply']>>;
export async function replyLocalized<TKeys extends TFunctionKeys = string, TInterpolationMap extends NonNullObject = StringMap>(
target: (BaseCommandInteraction | Message | MessageComponentInteraction) | Message,
options: TKeys | TKeys[] | LocalizedMessageOptions<TKeys, TInterpolationMap> | LocalizedInteractionReplyOptions<TKeys, TInterpolationMap>
): Promise<
ReturnType<(BaseCommandInteraction | MessageComponentInteraction)['reply']> | ReturnType<MessageComponentInteraction['update']> | Message
> {
if (target instanceof BaseCommandInteraction || target instanceof MessageComponentInteraction) {
if (target.deferred || target.replied) {
return target.editReply(await resolveOverloads(target, options)) as Promise<Message<boolean>>;
}

if (target.isMessageComponent()) {
return target.reply(await resolveOverloads(target, options));
}
}

return target.reply(await resolveOverloads(target, options));
}

/**
* Edits a message using the language `keys` from your i18next language setup.
* @since 2.0.0
* @param target The message to edit.
* @param keys The language keys to be sent.
* @example
* ```typescript
* // Using a string to specify the key to send
* await editLocalized(message, 'commands/ping:fail');
* // ➡ "Pong!"
* ```
*/
export async function editLocalized<TKeys extends TFunctionKeys = string>(target: Message, keys: TKeys | TKeys[]): Promise<Message>;
/**
* Edits a message using {@link LocalizedMessageOptions}.
* @since 2.0.0
* @param target The message to edit.
* @param options A {@link LocalizedMessageOptions} object, requiring at least a `keys` field.
* @example
* ```typescript
* // Using an object to specify the key to send
* await editLocalized(message, { keys: 'commands/ping:fail' });
* // ➡ "Pong!"
* ```
* @example
* ```typescript
* // Passing interpolation options into i18next
* const latency = 42;
* const took = 96;
*
* await editLocalized(message, {
* keys: 'commands/ping:success',
* formatOptions: { latency, took }
* });
* // ➡ "Pong! Took me 96ms to reply, and my heart took 42ms to beat!"
* ```
*/
export async function editLocalized<TKeys extends TFunctionKeys = string, TInterpolationMap extends NonNullObject = StringMap>(
target: Message,
options: LocalizedMessageOptions<TKeys, TInterpolationMap>
): Promise<Message>;
/**
* Edits a reply to an interaction, optionally deferred, using the given language.
* @since 2.4.0
* @param target The interaction to editReply.
* @param options The language keys that will resolve to the new interaction content.
* @example
* ```typescript
* // Using a string to specify the key to send
* await editLocalized(interaction, 'commands/ping:fail');
* // ➡ "Pong!"
* ```
*/
export async function editLocalized<TKeys extends TFunctionKeys = string>(
target: BaseCommandInteraction | MessageComponentInteraction,
keys: TKeys | TKeys[]
): Promise<ReturnType<(BaseCommandInteraction | MessageComponentInteraction)['editReply']>>;
/**
* Edits a reply to an interaction, optionally deferred, using {@link LocalizedInteractionReplyOptions}.
* @since 2.4.0
* @param target The interaction to editReply.
* @param options A {@link LocalizedInteractionReplyOptions} object, requiring at least a `keys` field.
* @example
* ```typescript
* // Using an object to specify the key to send
* await editLocalized(interaction, { keys: 'commands/ping:fail' });
* // ➡ "Pong!"
* ```
* @example
* ```typescript
* // Passing interpolation options into i18next
* const latency = 42;
* const took = 96;
*
* await editLocalized(interaction, {
* keys: 'commands/ping:success',
* formatOptions: { latency, took }
* });
* // ➡ "Pong! Took me 96ms to reply, and my heart took 42ms to beat!"
* ```
*/
export async function editLocalized<TKeys extends TFunctionKeys = string, TInterpolationMap extends NonNullObject = StringMap>(
target: BaseCommandInteraction | MessageComponentInteraction,
options: LocalizedInteractionReplyOptions<TKeys, TInterpolationMap>
): Promise<ReturnType<(BaseCommandInteraction | MessageComponentInteraction)['editReply']>>;
export async function editLocalized<TKeys extends TFunctionKeys = string, TInterpolationMap extends NonNullObject = StringMap>(
target: BaseCommandInteraction | Message | MessageComponentInteraction,
options: TKeys | TKeys[] | LocalizedMessageOptions<TKeys, TInterpolationMap> | LocalizedInteractionReplyOptions<TKeys, TInterpolationMap>
): Promise<
| ReturnType<(BaseCommandInteraction | MessageComponentInteraction)['editReply']>
| ReturnType<(BaseCommandInteraction | MessageComponentInteraction)['reply']>
| Message
> {
if (target instanceof BaseCommandInteraction || target instanceof MessageComponentInteraction) {
if (target.deferred || target.replied) {
return target.editReply(await resolveOverloads(target, options));
}

return target.reply(await resolveOverloads(target, options));
}

return target.edit(await resolveOverloads(target, options));
}

/**
* @internal
*/
async function resolveLanguage(context: InternationalizationContext): Promise<string> {
Object.defineProperty(context, 'author', {
get: deprecate(
() => {
return context.user;
},
"InternationalizationContext's `author` property is deprecated and will be removed in the next major version. Please use `InternationalizationContext.user` instead.",
'DeprecationWarning'
),
set: deprecate(
(val: InternationalizationContext['user']) => {
context.user = val;
},
"InternationalizationContext's `author` property is deprecated and will be removed in the next major version. Please use `InternationalizationContext.user` instead.",
'DeprecationWarning'
)
});

const lang = await container.i18n.fetchLanguage(context);
return lang ?? context.guild?.preferredLocale ?? container.i18n.options.defaultName ?? 'en-US';
}

/**
* @internal
*/
function resolveTextChannel(target: ChannelTarget): TextBasedDiscordChannel {
if (target instanceof Message) return target.channel;
if (target.isText()) return target;
throw new TypeError(`Cannot resolve ${target.name} to a text-based channel.`);
}

/**
* @internal
*/
async function resolveOverloads<TKeys extends TFunctionKeys = string, TInterpolationMap extends NonNullObject = StringMap>(
target: Target,
options: TKeys | TKeys[] | LocalizedMessageOptions<TKeys, TInterpolationMap> | LocalizedInteractionReplyOptions<TKeys, TInterpolationMap>
): Promise<Pick<Message, 'content'>> {
if (isObject(options)) {
const casted = cast<LocalizedMessageOptions<TKeys, TInterpolationMap> | LocalizedInteractionReplyOptions<TKeys, TInterpolationMap>>(options);
return { ...options, content: await resolveKey(target, casted.keys, casted.formatOptions) };
}

return { content: await resolveKey(target, options) };
}
30 changes: 2 additions & 28 deletions packages/i18next/src/lib/types.ts
@@ -1,20 +1,18 @@
import type { Awaitable, NonNullObject } from '@sapphire/utilities';
import type { Awaitable } from '@sapphire/utilities';
import type { Backend } from '@skyra/i18next-backend';
import type { WatchOptions } from 'chokidar';
import type {
BaseCommandInteraction,
Guild,
Interaction,
InteractionReplyOptions,
Message,
MessageComponentInteraction,
MessageOptions,
StageChannel,
StoreChannel,
User,
VoiceChannel
} from 'discord.js';
import type { InitOptions, StringMap, TFunctionKeys, TOptions } from 'i18next';
import type { InitOptions } from 'i18next';

/**
* Configure whether to use Hot-Module-Replacement (HMR) for your i18next resources using these options. The minimum config to enable HMR is to set `enabled` to true. Any other properties are optional.
Expand Down Expand Up @@ -137,11 +135,6 @@ export interface InternationalizationContext {
guild: Guild | null;
/** The {@link DiscordChannel} object to fetch the preferred language for. */
channel: DiscordChannel | null;
/**
* @deprecated Use {@link InternationalizationContext.user} instead; this will be removed in the next major version.
* The user to fetch the preferred language for.
*/
author?: User | null;
/** The user to fetch the preferred language for. */
user: User | null;
interactionGuildLocale?: Interaction['guildLocale'];
Expand All @@ -157,24 +150,5 @@ export interface I18nextFormatters {
format(value: any, lng: string | undefined, options: any): string;
}

export interface LocalizedInteractionReplyOptions<TKeys extends TFunctionKeys = string, TInterpolationMap extends NonNullObject = StringMap>
extends PartialLocalizedInteractionReplyOptions<TInterpolationMap> {
keys: TKeys | TKeys[];
}

export interface LocalizedMessageOptions<TKeys extends TFunctionKeys = string, TInterpolationMap extends NonNullObject = StringMap>
extends PartialLocalizedMessageOptions<TInterpolationMap> {
keys: TKeys | TKeys[];
}

export interface PartialLocalizedInteractionReplyOptions<TInterpolationMap extends NonNullObject = StringMap>
extends Omit<InteractionReplyOptions, 'content'> {
formatOptions?: TOptions<TInterpolationMap>;
}

export interface PartialLocalizedMessageOptions<TInterpolationMap extends NonNullObject = StringMap> extends Omit<MessageOptions, 'content'> {
formatOptions?: TOptions<TInterpolationMap>;
}

export type ChannelTarget = Message | DiscordChannel;
export type Target = BaseCommandInteraction | ChannelTarget | Guild | MessageComponentInteraction;

0 comments on commit e53558f

Please sign in to comment.