@wolfstar/plugin-i18next 1.0.0-next-20260829115416 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -110,6 +110,41 @@ const t = await fetchT(interaction);
110
110
  const content = await fetchKey(interaction, "commands/ping:success");
111
111
  ```
112
112
 
113
+ ### Targets
114
+
115
+ Every helper accepts any of four raw payloads, told apart structurally — the framework never has
116
+ `discord.js` class instances to check with `instanceof`:
117
+
118
+ | Target | Recognized by | Locale it carries |
119
+ | --------------- | ------------- | --------------------------- |
120
+ | `Interaction` | `locale` | `locale` and `guild_locale` |
121
+ | `MessageTarget` | `channel_id` | none |
122
+ | `ChannelTarget` | `type` | none |
123
+ | `GuildTarget` | anything else | `preferred_locale` |
124
+
125
+ `APIInteraction`, `APIMessage`, `APIChannel` and `APIGuild` all satisfy the matching target, so
126
+ payloads can be passed straight through:
127
+
128
+ ```typescript
129
+ import { getSupportedLanguageName, resolveKey } from "@wolfstar/plugin-i18next";
130
+
131
+ // A guild resolves through its `preferred_locale`:
132
+ const guild = await container.rest.get(Routes.guild(guildId));
133
+ const language = getSupportedLanguageName(guild);
134
+ const content = resolveKey(guild, Success);
135
+ ```
136
+
137
+ Channels and messages carry no locale of their own, so the synchronous helpers fall back to
138
+ `defaultName` for them. Use the asynchronous `fetch*` helpers instead: the hook receives the
139
+ target's `guildId`, `channelId` and `userId`, which is enough to look the language up.
140
+
141
+ ```typescript
142
+ container.i18n.fetchLanguage = async ({ guildId }) =>
143
+ guildId ? ((await database.getGuild(guildId))?.language ?? null) : null;
144
+
145
+ const content = await fetchKey(message, "commands/ping:success");
146
+ ```
147
+
113
148
  ### Localizing command builders
114
149
 
115
150
  ```typescript
@@ -1,5 +1,5 @@
1
1
  import i18next, { AppendKeyPrefix, DefaultNamespace, InitOptions, InitOptions as InitOptions$1, InterpolationMap, Namespace, ParseKeys, TFunction, TFunction as TFunction$1, TFunctionReturn, TFunctionReturnOptionalDetails, TOptions, TOptions as TOptions$1, TOptionsBase, TOptionsBase as TOptionsBase$1 } from "i18next";
2
- import { APIApplicationCommandOptionChoice, APIInteraction, APIPingInteraction, LocaleString, LocalizationMap } from "discord-api-types/v10";
2
+ import { APIApplicationCommandOptionChoice, APIInteraction, APIPingInteraction, APIUser, ChannelType, LocaleString, LocalizationMap } from "discord-api-types/v10";
3
3
  import { Awaitable, NonNullObject } from "@sapphire/utilities";
4
4
  import { Backend } from "@wolfstar/i18next-backend";
5
5
  import { ChokidarOptions } from "chokidar";
@@ -63,10 +63,57 @@ type LocalePrefixKey = `commands/${string}:${string}`;
63
63
  * operate on `discord-api-types` structures rather than on `discord.js` class instances.
64
64
  */
65
65
  type Interaction = Pick<Exclude<APIInteraction, APIPingInteraction>, "locale" | "guild_locale" | "guild_id"> & Partial<Pick<Exclude<APIInteraction, APIPingInteraction>, "channel_id" | "user" | "member">>;
66
+ /**
67
+ * The subset of a guild payload the localization helpers need.
68
+ *
69
+ * @remarks
70
+ * `APIGuild` satisfies it, as does any partial guild that carries an id. Only a full guild payload
71
+ * carries `preferred_locale`; without it the guild's id is still handed to
72
+ * {@link InternationalizationHandler.fetchLanguage}, so a custom hook can resolve the language.
73
+ */
74
+ interface GuildTarget {
75
+ id: string;
76
+ /**
77
+ * The guild's preferred locale. Typed as `string` rather than `LocaleString` because
78
+ * `APIGuild` types it as the `Locale` enum; it is validated at runtime.
79
+ */
80
+ preferred_locale?: string;
81
+ }
82
+ /**
83
+ * The subset of a channel payload the localization helpers need. Every `APIChannel` member
84
+ * satisfies it.
85
+ *
86
+ * @remarks
87
+ * A channel payload carries no locale of its own, so the language is resolved from
88
+ * {@link InternationalizationHandler.fetchLanguage} and then from the configured default.
89
+ */
90
+ interface ChannelTarget {
91
+ id: string;
92
+ type: ChannelType;
93
+ guild_id?: string;
94
+ }
95
+ /**
96
+ * The subset of a message payload the localization helpers need. `APIMessage` satisfies it, and
97
+ * gateway message payloads additionally carry `guild_id`.
98
+ *
99
+ * @remarks
100
+ * Like {@link ChannelTarget}, a message carries no locale of its own.
101
+ */
102
+ interface MessageTarget {
103
+ id: string;
104
+ channel_id: string;
105
+ guild_id?: string;
106
+ author?: Pick<APIUser, "id">;
107
+ }
66
108
  /**
67
109
  * Any value the localization helpers accept as the source of a language.
110
+ *
111
+ * @remarks
112
+ * The four members are told apart structurally, in this order: an {@link Interaction} has `locale`,
113
+ * a {@link MessageTarget} has `channel_id`, a {@link ChannelTarget} has `type`, and anything else is
114
+ * a {@link GuildTarget}.
68
115
  */
69
- type Target = Interaction;
116
+ type Target = Interaction | MessageTarget | ChannelTarget | GuildTarget;
70
117
  /**
71
118
  * Configure whether to use Hot-Module-Replacement (HMR) for your i18next resources using these
72
119
  * options. The minimum config to enable HMR is to set `enabled` to true. Any other properties are
@@ -175,6 +222,15 @@ interface InternationalizationContext {
175
222
  interactionGuildLocale?: LocaleString;
176
223
  /** The locale the user that sent the interaction is configured with. */
177
224
  interactionLocale?: LocaleString;
225
+ /**
226
+ * The preferred locale of the guild the target belongs to.
227
+ *
228
+ * @remarks
229
+ * Filled in from `guild_locale` for an {@link Interaction} and from `preferred_locale` for a
230
+ * {@link GuildTarget}; a {@link ChannelTarget} and a {@link MessageTarget} carry no locale, so it
231
+ * is left undefined for them.
232
+ */
233
+ preferredLocale?: string;
178
234
  }
179
235
  interface InternationalizationClientOptions {
180
236
  i18n?: InternationalizationOptions;
@@ -389,32 +445,47 @@ declare function isSupportedDiscordLocale(language: string): language is LocaleS
389
445
  /**
390
446
  * Resolves the loaded language that best matches the user's locale, falling back to the guild's one,
391
447
  * then to {@link InternationalizationOptions.defaultName}, and finally to `'en-US'`.
392
- * @param interaction The interaction to read the locales from.
448
+ *
449
+ * @remarks
450
+ * Only an {@link Interaction} carries a user locale. For a {@link GuildTarget} this is equivalent to
451
+ * {@link getSupportedLanguageName}, and a {@link ChannelTarget} or {@link MessageTarget} always
452
+ * resolves to the fallback.
453
+ * @param target The target to read the locales from.
393
454
  */
394
- declare function getSupportedUserLanguageName(interaction: Interaction): LocaleString;
455
+ declare function getSupportedUserLanguageName(target: Target): LocaleString;
395
456
  /**
396
457
  * Resolves the `TFunction` for {@link getSupportedUserLanguageName}.
397
- * @param interaction The interaction to read the locales from.
458
+ * @param target The target to read the locales from.
398
459
  */
399
- declare function getSupportedUserLanguageT(interaction: Interaction): TFunction$1;
460
+ declare function getSupportedUserLanguageT(target: Target): TFunction$1;
400
461
  /**
401
462
  * Resolves the loaded language that best matches the guild's locale, falling back to the user's one
402
- * when the interaction was not sent from a guild, then to
463
+ * when the target does not belong to a guild, then to
403
464
  * {@link InternationalizationOptions.defaultName}, and finally to `'en-US'`.
404
- * @param interaction The interaction to read the locales from.
465
+ *
466
+ * @remarks
467
+ * The guild locale comes from `guild_locale` on an {@link Interaction} and from `preferred_locale`
468
+ * on a {@link GuildTarget}. A {@link ChannelTarget} and a {@link MessageTarget} carry no locale, so
469
+ * they resolve to the fallback unless a custom
470
+ * {@link InternationalizationHandler.fetchLanguage} hook is used through {@link fetchLanguage}.
471
+ * @param target The target to read the locales from.
405
472
  */
406
- declare function getSupportedLanguageName(interaction: Interaction): LocaleString;
473
+ declare function getSupportedLanguageName(target: Target): LocaleString;
407
474
  /**
408
475
  * Resolves the `TFunction` for {@link getSupportedLanguageName}.
409
- * @param interaction The interaction to read the locales from.
476
+ * @param target The target to read the locales from.
410
477
  */
411
- declare function getSupportedLanguageT(interaction: Interaction): TFunction$1;
478
+ declare function getSupportedLanguageT(target: Target): TFunction$1;
412
479
  /**
413
480
  * Retrieves the language name for a target, using {@link InternationalizationHandler.fetchLanguage}.
414
481
  *
415
482
  * If that hook is not defined or returns a nullish value, the language is resolved from the
416
- * interaction's locales through {@link getSupportedLanguageName}, which itself falls back to
483
+ * locales the target carries through {@link getSupportedLanguageName}, which itself falls back to
417
484
  * {@link InternationalizationOptions.defaultName} and then to `'en-US'`.
485
+ * @remarks
486
+ * This is the only helper that can resolve a language for a {@link ChannelTarget} or a
487
+ * {@link MessageTarget}, since those payloads carry no locale and the hook receives their
488
+ * `guildId` and `channelId`.
418
489
  * @param target The target to fetch the language from.
419
490
  */
420
491
  declare function fetchLanguage(target: Target): Promise<string>;
@@ -429,8 +500,8 @@ declare function fetchT(target: Target): Promise<TFunction$1>;
429
500
  * is honoured.
430
501
  *
431
502
  * @remarks
432
- * Use {@link resolveKey} when the language can be resolved from the interaction payload alone, it
433
- * is synchronous and does not hit the hook.
503
+ * Use {@link resolveKey} when the language can be resolved from the target payload alone, it is
504
+ * synchronous and does not hit the hook.
434
505
  * @param target The target to fetch the language key from.
435
506
  */
436
507
  declare function fetchKey<const Key extends ParseKeys<Ns, TOpt, undefined>, const TOpt extends TOptions$1 = TOptions$1, Ns extends Namespace = DefaultNamespace, Ret extends TFunctionReturn<Ns, AppendKeyPrefix<Key, undefined>, TOpt> = TOpt["returnObjects"] extends true ? $SpecialObject : string, const ActualOptions extends TOpt & InterpolationMap<Ret> = TOpt & InterpolationMap<Ret>>(target: Target, ...[key, defaultValueOrOptions, optionsOrUndefined]: [key: Key | Key[], options?: ActualOptions] | [key: string | string[], options: TOpt & $Dictionary & {
@@ -439,19 +510,19 @@ declare function fetchKey<const Key extends ParseKeys<Ns, TOpt, undefined>, cons
439
510
  /**
440
511
  * Resolves a key with the user's language, as resolved by {@link getSupportedUserLanguageName}.
441
512
  */
442
- declare function resolveUserKey<TReturn>(interaction: Interaction, key: TypedT<TReturn>, options?: TOptionsBase$1 | string): TReturn;
443
- declare function resolveUserKey<TReturn>(interaction: Interaction, key: TypedT<TReturn>, defaultValue: TReturn, options?: TOptionsBase$1 | string): TReturn;
444
- declare function resolveUserKey<TArgs extends NonNullObject, TReturn>(interaction: Interaction, key: TypedFT<TArgs, TReturn>, options?: TOptions$1<TArgs>): TReturn;
445
- declare function resolveUserKey<TArgs extends NonNullObject, TReturn>(interaction: Interaction, key: TypedFT<TArgs, TReturn>, defaultValue: TReturn, options?: TOptions$1<TArgs>): TReturn;
446
- declare function resolveUserKey(interaction: Interaction, key: string | string[], ...args: [any?, any?]): string;
513
+ declare function resolveUserKey<TReturn>(target: Target, key: TypedT<TReturn>, options?: TOptionsBase$1 | string): TReturn;
514
+ declare function resolveUserKey<TReturn>(target: Target, key: TypedT<TReturn>, defaultValue: TReturn, options?: TOptionsBase$1 | string): TReturn;
515
+ declare function resolveUserKey<TArgs extends NonNullObject, TReturn>(target: Target, key: TypedFT<TArgs, TReturn>, options?: TOptions$1<TArgs>): TReturn;
516
+ declare function resolveUserKey<TArgs extends NonNullObject, TReturn>(target: Target, key: TypedFT<TArgs, TReturn>, defaultValue: TReturn, options?: TOptions$1<TArgs>): TReturn;
517
+ declare function resolveUserKey(target: Target, key: string | string[], ...args: [any?, any?]): string;
447
518
  /**
448
519
  * Resolves a key with the guild's language, as resolved by {@link getSupportedLanguageName}.
449
520
  */
450
- declare function resolveKey<TReturn>(interaction: Interaction, key: TypedT<TReturn>, options?: TOptionsBase$1 | string): TReturn;
451
- declare function resolveKey<TReturn>(interaction: Interaction, key: TypedT<TReturn>, defaultValue: TReturn, options?: TOptionsBase$1 | string): TReturn;
452
- declare function resolveKey<TArgs extends NonNullObject, TReturn>(interaction: Interaction, key: TypedFT<TArgs, TReturn>, options?: TOptions$1<TArgs>): TReturn;
453
- declare function resolveKey<TArgs extends NonNullObject, TReturn>(interaction: Interaction, key: TypedFT<TArgs, TReturn>, defaultValue: TReturn, options?: TOptions$1<TArgs>): TReturn;
454
- declare function resolveKey(interaction: Interaction, key: string | string[], ...args: [any?, any?]): string;
521
+ declare function resolveKey<TReturn>(target: Target, key: TypedT<TReturn>, options?: TOptionsBase$1 | string): TReturn;
522
+ declare function resolveKey<TReturn>(target: Target, key: TypedT<TReturn>, defaultValue: TReturn, options?: TOptionsBase$1 | string): TReturn;
523
+ declare function resolveKey<TArgs extends NonNullObject, TReturn>(target: Target, key: TypedFT<TArgs, TReturn>, options?: TOptions$1<TArgs>): TReturn;
524
+ declare function resolveKey<TArgs extends NonNullObject, TReturn>(target: Target, key: TypedFT<TArgs, TReturn>, defaultValue: TReturn, options?: TOptions$1<TArgs>): TReturn;
525
+ declare function resolveKey(target: Target, key: string | string[], ...args: [any?, any?]): string;
455
526
  /**
456
527
  * Gets the value and the localizations from a language key.
457
528
  * @param key The key to get the localizations from.
@@ -541,5 +612,5 @@ declare module "@wolfstar/http-framework" {
541
612
  }
542
613
  }
543
614
  //#endregion
544
- export { type $Dictionary, type $NoInfer, type $SpecialObject, type BuilderWithDescription, type BuilderWithName, type BuilderWithNameAndDescription, type Difference, type DynamicOptions, FT, type HMROptions, type I18nextFormatter, type I18nextNamedCachedFormatter, type I18nextNamedFormatter, type InitOptions, type Interaction, type InternationalizationClientOptions, type InternationalizationContext, InternationalizationHandler, type InternationalizationOptions, type LocalePrefixKey, type LocalizedData, T, type TFunction, type TOptions, type TOptionsBase, type Target, type TypedFT, type TypedT, type Value, type Values, applyDescriptionLocalizedBuilder, applyLocalizedBuilder, applyNameLocalizedBuilder, createLocalizedChoice, createSelectMenuChoiceName, fetchKey, fetchLanguage, fetchT, getLocalizedData, getSupportedLanguageName, getSupportedLanguageT, getSupportedUserLanguageName, getSupportedUserLanguageT, i18next, isSupportedDiscordLocale, resolveKey, resolveUserKey, supportedLanguages };
615
+ export { type $Dictionary, type $NoInfer, type $SpecialObject, type BuilderWithDescription, type BuilderWithName, type BuilderWithNameAndDescription, type ChannelTarget, type Difference, type DynamicOptions, FT, type GuildTarget, type HMROptions, type I18nextFormatter, type I18nextNamedCachedFormatter, type I18nextNamedFormatter, type InitOptions, type Interaction, type InternationalizationClientOptions, type InternationalizationContext, InternationalizationHandler, type InternationalizationOptions, type LocalePrefixKey, type LocalizedData, type MessageTarget, T, type TFunction, type TOptions, type TOptionsBase, type Target, type TypedFT, type TypedT, type Value, type Values, applyDescriptionLocalizedBuilder, applyLocalizedBuilder, applyNameLocalizedBuilder, createLocalizedChoice, createSelectMenuChoiceName, fetchKey, fetchLanguage, fetchT, getLocalizedData, getSupportedLanguageName, getSupportedLanguageT, getSupportedUserLanguageName, getSupportedUserLanguageT, i18next, isSupportedDiscordLocale, resolveKey, resolveUserKey, supportedLanguages };
545
616
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/lib/types.ts","../../src/lib/InternationalizationHandler.ts","../../src/lib/functions.ts","../../src/index.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/lib/types.ts","../../src/lib/InternationalizationHandler.ts","../../src/lib/functions.ts","../../src/index.ts"],"mappings":"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"}
package/dist/esm/index.js CHANGED
@@ -43,7 +43,7 @@ function isSupportedDiscordLocale(language) {
43
43
  return supportedLanguages.has(language);
44
44
  }
45
45
  /**
46
- * Resolves the fallback language to use when none of an interaction's locales is loaded.
46
+ * Resolves the fallback language to use when none of a target's locales is loaded.
47
47
  *
48
48
  * {@link InternationalizationOptions.defaultName} wins as long as it is both a Discord locale and a
49
49
  * loaded language; otherwise `'en-US'` is used.
@@ -55,66 +55,118 @@ function getFallbackLanguageName() {
55
55
  return defaultName && isSupportedDiscordLocale(defaultName) && languages.has(defaultName) ? defaultName : "en-US";
56
56
  }
57
57
  /**
58
+ * Narrows a locale to a loaded Discord locale, or `null` when it is neither.
59
+ * @internal
60
+ */
61
+ function getLoadedLocale(locale) {
62
+ if (!locale) return null;
63
+ return isSupportedDiscordLocale(locale) && container.i18n.languages.has(locale) ? locale : null;
64
+ }
65
+ /**
66
+ * Builds the {@link InternationalizationContext} for any supported target.
67
+ *
68
+ * @remarks
69
+ * The members of {@link Target} are told apart structurally, since the framework receives raw
70
+ * payloads rather than class instances: an interaction has `locale`, a message has `channel_id`, a
71
+ * channel has `type`, and anything left is a guild.
72
+ * @internal
73
+ */
74
+ function resolveContext(target) {
75
+ if ("locale" in target) return {
76
+ guildId: target.guild_id ?? null,
77
+ channelId: target.channel_id ?? null,
78
+ userId: target.user?.id ?? target.member?.user.id ?? null,
79
+ interactionGuildLocale: target.guild_locale,
80
+ interactionLocale: target.locale,
81
+ preferredLocale: target.guild_locale
82
+ };
83
+ if ("channel_id" in target) return {
84
+ guildId: target.guild_id ?? null,
85
+ channelId: target.channel_id,
86
+ userId: target.author?.id ?? null
87
+ };
88
+ if ("type" in target) return {
89
+ guildId: target.guild_id ?? null,
90
+ channelId: target.id,
91
+ userId: null
92
+ };
93
+ return {
94
+ guildId: target.id,
95
+ channelId: null,
96
+ userId: null,
97
+ preferredLocale: target.preferred_locale
98
+ };
99
+ }
100
+ /**
101
+ * @internal
102
+ */
103
+ function getSupportedLanguageNameFromContext(context) {
104
+ return (context.guildId ? getLoadedLocale(context.preferredLocale) : getLoadedLocale(context.interactionLocale)) ?? getFallbackLanguageName();
105
+ }
106
+ /**
107
+ * @internal
108
+ */
109
+ function getSupportedUserLanguageNameFromContext(context) {
110
+ return getLoadedLocale(context.interactionLocale) ?? getLoadedLocale(context.preferredLocale) ?? getFallbackLanguageName();
111
+ }
112
+ /**
58
113
  * Resolves the loaded language that best matches the user's locale, falling back to the guild's one,
59
114
  * then to {@link InternationalizationOptions.defaultName}, and finally to `'en-US'`.
60
- * @param interaction The interaction to read the locales from.
115
+ *
116
+ * @remarks
117
+ * Only an {@link Interaction} carries a user locale. For a {@link GuildTarget} this is equivalent to
118
+ * {@link getSupportedLanguageName}, and a {@link ChannelTarget} or {@link MessageTarget} always
119
+ * resolves to the fallback.
120
+ * @param target The target to read the locales from.
61
121
  */
62
- function getSupportedUserLanguageName(interaction) {
63
- const { languages } = container.i18n;
64
- if (languages.has(interaction.locale)) return interaction.locale;
65
- if (interaction.guild_locale && languages.has(interaction.guild_locale)) return interaction.guild_locale;
66
- return getFallbackLanguageName();
122
+ function getSupportedUserLanguageName(target) {
123
+ return getSupportedUserLanguageNameFromContext(resolveContext(target));
67
124
  }
68
125
  /**
69
126
  * Resolves the `TFunction` for {@link getSupportedUserLanguageName}.
70
- * @param interaction The interaction to read the locales from.
127
+ * @param target The target to read the locales from.
71
128
  */
72
- function getSupportedUserLanguageT(interaction) {
73
- return container.i18n.getT(getSupportedUserLanguageName(interaction));
129
+ function getSupportedUserLanguageT(target) {
130
+ return container.i18n.getT(getSupportedUserLanguageName(target));
74
131
  }
75
132
  /**
76
133
  * Resolves the loaded language that best matches the guild's locale, falling back to the user's one
77
- * when the interaction was not sent from a guild, then to
134
+ * when the target does not belong to a guild, then to
78
135
  * {@link InternationalizationOptions.defaultName}, and finally to `'en-US'`.
79
- * @param interaction The interaction to read the locales from.
136
+ *
137
+ * @remarks
138
+ * The guild locale comes from `guild_locale` on an {@link Interaction} and from `preferred_locale`
139
+ * on a {@link GuildTarget}. A {@link ChannelTarget} and a {@link MessageTarget} carry no locale, so
140
+ * they resolve to the fallback unless a custom
141
+ * {@link InternationalizationHandler.fetchLanguage} hook is used through {@link fetchLanguage}.
142
+ * @param target The target to read the locales from.
80
143
  */
81
- function getSupportedLanguageName(interaction) {
82
- const { languages } = container.i18n;
83
- if (interaction.guild_id) {
84
- if (interaction.guild_locale && languages.has(interaction.guild_locale)) return interaction.guild_locale;
85
- } else if (languages.has(interaction.locale)) return interaction.locale;
86
- return getFallbackLanguageName();
144
+ function getSupportedLanguageName(target) {
145
+ return getSupportedLanguageNameFromContext(resolveContext(target));
87
146
  }
88
147
  /**
89
148
  * Resolves the `TFunction` for {@link getSupportedLanguageName}.
90
- * @param interaction The interaction to read the locales from.
149
+ * @param target The target to read the locales from.
91
150
  */
92
- function getSupportedLanguageT(interaction) {
93
- return container.i18n.getT(getSupportedLanguageName(interaction));
94
- }
95
- /**
96
- * Builds the {@link InternationalizationContext} for an interaction.
97
- * @internal
98
- */
99
- function getContext(interaction) {
100
- return {
101
- guildId: interaction.guild_id ?? null,
102
- channelId: interaction.channel_id ?? null,
103
- userId: interaction.user?.id ?? interaction.member?.user.id ?? null,
104
- interactionGuildLocale: interaction.guild_locale,
105
- interactionLocale: interaction.locale
106
- };
151
+ function getSupportedLanguageT(target) {
152
+ return container.i18n.getT(getSupportedLanguageName(target));
107
153
  }
108
154
  /**
109
155
  * Retrieves the language name for a target, using {@link InternationalizationHandler.fetchLanguage}.
110
156
  *
111
157
  * If that hook is not defined or returns a nullish value, the language is resolved from the
112
- * interaction's locales through {@link getSupportedLanguageName}, which itself falls back to
158
+ * locales the target carries through {@link getSupportedLanguageName}, which itself falls back to
113
159
  * {@link InternationalizationOptions.defaultName} and then to `'en-US'`.
160
+
161
+ * @remarks
162
+ * This is the only helper that can resolve a language for a {@link ChannelTarget} or a
163
+ * {@link MessageTarget}, since those payloads carry no locale and the hook receives their
164
+ * `guildId` and `channelId`.
114
165
  * @param target The target to fetch the language from.
115
166
  */
116
167
  async function fetchLanguage(target) {
117
- return await container.i18n.fetchLanguage(getContext(target)) ?? getSupportedLanguageName(target);
168
+ const context = resolveContext(target);
169
+ return await container.i18n.fetchLanguage(context) ?? getSupportedLanguageNameFromContext(context);
118
170
  }
119
171
  /**
120
172
  * Retrieves the language-assigned function from i18next designated to a target's preferred language.
@@ -129,8 +181,8 @@ async function fetchT(target) {
129
181
  * is honoured.
130
182
  *
131
183
  * @remarks
132
- * Use {@link resolveKey} when the language can be resolved from the interaction payload alone, it
133
- * is synchronous and does not hit the hook.
184
+ * Use {@link resolveKey} when the language can be resolved from the target payload alone, it is
185
+ * synchronous and does not hit the hook.
134
186
  * @param target The target to fetch the language key from.
135
187
  */
136
188
  async function fetchKey(target, ...[key, defaultValueOrOptions, optionsOrUndefined]) {
@@ -139,11 +191,11 @@ async function fetchKey(target, ...[key, defaultValueOrOptions, optionsOrUndefin
139
191
  if (typeof defaultValueOrOptions === "string") return container.i18n.format(language, key, defaultValueOrOptions, optionsOrUndefined);
140
192
  return container.i18n.format(language, key, void 0, defaultValueOrOptions);
141
193
  }
142
- function resolveUserKey(interaction, ...args) {
143
- return getSupportedUserLanguageT(interaction)(...args);
194
+ function resolveUserKey(target, ...args) {
195
+ return getSupportedUserLanguageT(target)(...args);
144
196
  }
145
- function resolveKey(interaction, ...args) {
146
- return getSupportedLanguageT(interaction)(...args);
197
+ function resolveKey(target, ...args) {
198
+ return getSupportedLanguageT(target)(...args);
147
199
  }
148
200
  let cachedLocales = null;
149
201
  let cachedLocalesSize = -1;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["i18next"],"sources":["../../src/lib/functions.ts","../../src/lib/InternationalizationHandler.ts"],"sourcesContent":["import { Collection } from \"@discordjs/collection\";\nimport { container } from \"@sapphire/pieces\";\nimport type { NonNullObject } from \"@sapphire/utilities\";\nimport {\n Locale,\n type APIApplicationCommandOptionChoice,\n type LocaleString,\n} from \"discord-api-types/v10\";\nimport type {\n AppendKeyPrefix,\n DefaultNamespace,\n InterpolationMap,\n Namespace,\n ParseKeys,\n TFunction,\n TFunctionReturn,\n TFunctionReturnOptionalDetails,\n TOptions,\n TOptionsBase,\n} from \"i18next\";\nimport type {\n $Dictionary,\n $SpecialObject,\n BuilderWithDescription,\n BuilderWithName,\n BuilderWithNameAndDescription,\n Interaction,\n InternationalizationContext,\n LocalePrefixKey,\n LocalizedData,\n Target,\n TypedFT,\n TypedT,\n} from \"./types\";\n\n/**\n * Brands a translation key with the type it resolves to.\n * @param k The i18next key.\n * @example\n * ```typescript\n * export const InvalidInput = T('path/to/file:invalidInput');\n * ```\n */\nexport function T<TCustom = string>(k: string): TypedT<TCustom> {\n return k as TypedT<TCustom>;\n}\n\n/**\n * Brands a translation key with both its interpolation arguments and the type it resolves to.\n * @param k The i18next key.\n * @example\n * ```typescript\n * export const AddResult = FT<{ left: number; right: number; result: number }>('path/to/file:addResult');\n * ```\n */\nexport function FT<TArgs extends NonNullObject = NonNullObject, TReturn = string>(\n k: string,\n): TypedFT<TArgs, TReturn> {\n return k as TypedFT<TArgs, TReturn>;\n}\n\n/**\n * Every locale Discord supports.\n */\nexport const supportedLanguages = new Set(Object.values(Locale)) as ReadonlySet<LocaleString>;\n\n/**\n * Checks whether the given language is a locale Discord supports.\n * @param language The language to check.\n */\nexport function isSupportedDiscordLocale(language: string): language is LocaleString {\n return supportedLanguages.has(language as LocaleString);\n}\n\n/**\n * Resolves the fallback language to use when none of an interaction's locales is loaded.\n *\n * {@link InternationalizationOptions.defaultName} wins as long as it is both a Discord locale and a\n * loaded language; otherwise `'en-US'` is used.\n * @internal\n */\nfunction getFallbackLanguageName(): LocaleString {\n const { languages, options } = container.i18n;\n const { defaultName } = options;\n\n return defaultName && isSupportedDiscordLocale(defaultName) && languages.has(defaultName)\n ? defaultName\n : \"en-US\";\n}\n\n/**\n * Resolves the loaded language that best matches the user's locale, falling back to the guild's one,\n * then to {@link InternationalizationOptions.defaultName}, and finally to `'en-US'`.\n * @param interaction The interaction to read the locales from.\n */\nexport function getSupportedUserLanguageName(interaction: Interaction): LocaleString {\n const { languages } = container.i18n;\n if (languages.has(interaction.locale)) return interaction.locale;\n if (interaction.guild_locale && languages.has(interaction.guild_locale)) {\n return interaction.guild_locale;\n }\n return getFallbackLanguageName();\n}\n\n/**\n * Resolves the `TFunction` for {@link getSupportedUserLanguageName}.\n * @param interaction The interaction to read the locales from.\n */\nexport function getSupportedUserLanguageT(interaction: Interaction): TFunction {\n return container.i18n.getT(getSupportedUserLanguageName(interaction));\n}\n\n/**\n * Resolves the loaded language that best matches the guild's locale, falling back to the user's one\n * when the interaction was not sent from a guild, then to\n * {@link InternationalizationOptions.defaultName}, and finally to `'en-US'`.\n * @param interaction The interaction to read the locales from.\n */\nexport function getSupportedLanguageName(interaction: Interaction): LocaleString {\n const { languages } = container.i18n;\n if (interaction.guild_id) {\n if (interaction.guild_locale && languages.has(interaction.guild_locale)) {\n return interaction.guild_locale;\n }\n } else if (languages.has(interaction.locale)) {\n return interaction.locale;\n }\n return getFallbackLanguageName();\n}\n\n/**\n * Resolves the `TFunction` for {@link getSupportedLanguageName}.\n * @param interaction The interaction to read the locales from.\n */\nexport function getSupportedLanguageT(interaction: Interaction): TFunction {\n return container.i18n.getT(getSupportedLanguageName(interaction));\n}\n\n/**\n * Builds the {@link InternationalizationContext} for an interaction.\n * @internal\n */\nfunction getContext(interaction: Interaction): InternationalizationContext {\n return {\n guildId: interaction.guild_id ?? null,\n channelId: interaction.channel_id ?? null,\n userId: interaction.user?.id ?? interaction.member?.user.id ?? null,\n interactionGuildLocale: interaction.guild_locale,\n interactionLocale: interaction.locale,\n };\n}\n\n/**\n * Retrieves the language name for a target, using {@link InternationalizationHandler.fetchLanguage}.\n *\n * If that hook is not defined or returns a nullish value, the language is resolved from the\n * interaction's locales through {@link getSupportedLanguageName}, which itself falls back to\n * {@link InternationalizationOptions.defaultName} and then to `'en-US'`.\n * @param target The target to fetch the language from.\n */\nexport async function fetchLanguage(target: Target): Promise<string> {\n const language = await container.i18n.fetchLanguage(getContext(target));\n return language ?? getSupportedLanguageName(target);\n}\n\n/**\n * Retrieves the language-assigned function from i18next designated to a target's preferred language.\n * @param target The target to fetch the language from.\n */\nexport async function fetchT(target: Target): Promise<TFunction> {\n return container.i18n.getT(await fetchLanguage(target));\n}\n\n/**\n * Resolves a key and its parameters using {@link fetchLanguage}, meaning a custom\n * {@link InternationalizationHandler.fetchLanguage} hook (for example, a per-guild database lookup)\n * is honoured.\n *\n * @remarks\n * Use {@link resolveKey} when the language can be resolved from the interaction payload alone, it\n * is synchronous and does not hit the hook.\n * @param target The target to fetch the language key from.\n */\nexport async function fetchKey<\n const Key extends ParseKeys<Ns, TOpt, undefined>,\n const TOpt extends TOptions = TOptions,\n Ns extends Namespace = DefaultNamespace,\n Ret extends TFunctionReturn<Ns, AppendKeyPrefix<Key, undefined>, TOpt> =\n TOpt[\"returnObjects\"] extends true ? $SpecialObject : string,\n const ActualOptions extends TOpt & InterpolationMap<Ret> = TOpt & InterpolationMap<Ret>,\n>(\n target: Target,\n ...[key, defaultValueOrOptions, optionsOrUndefined]:\n | [key: Key | Key[], options?: ActualOptions]\n | [key: string | string[], options: TOpt & $Dictionary & { defaultValue: string }]\n | [key: string | string[], defaultValue: string, options?: TOpt & $Dictionary]\n): Promise<TFunctionReturnOptionalDetails<Ret, TOpt>> {\n const parsedOptions =\n typeof defaultValueOrOptions === \"string\" ? optionsOrUndefined : defaultValueOrOptions;\n const language =\n typeof parsedOptions?.lng === \"string\" ? parsedOptions.lng : await fetchLanguage(target);\n\n if (typeof defaultValueOrOptions === \"string\") {\n return container.i18n.format<Key, TOpt, Ns, Ret>(\n language,\n key,\n defaultValueOrOptions,\n optionsOrUndefined,\n );\n }\n\n return container.i18n.format<Key, TOpt, Ns, Ret>(language, key, undefined, defaultValueOrOptions);\n}\n\n/**\n * Resolves a key with the user's language, as resolved by {@link getSupportedUserLanguageName}.\n */\nexport function resolveUserKey<TReturn>(\n interaction: Interaction,\n key: TypedT<TReturn>,\n options?: TOptionsBase | string,\n): TReturn;\nexport function resolveUserKey<TReturn>(\n interaction: Interaction,\n key: TypedT<TReturn>,\n defaultValue: TReturn,\n options?: TOptionsBase | string,\n): TReturn;\nexport function resolveUserKey<TArgs extends NonNullObject, TReturn>(\n interaction: Interaction,\n key: TypedFT<TArgs, TReturn>,\n options?: TOptions<TArgs>,\n): TReturn;\nexport function resolveUserKey<TArgs extends NonNullObject, TReturn>(\n interaction: Interaction,\n key: TypedFT<TArgs, TReturn>,\n defaultValue: TReturn,\n options?: TOptions<TArgs>,\n): TReturn;\nexport function resolveUserKey(\n interaction: Interaction,\n key: string | string[],\n ...args: [any?, any?]\n): string;\nexport function resolveUserKey(interaction: Interaction, ...args: [any, any?, any?]) {\n return (getSupportedUserLanguageT(interaction) as (...args: any[]) => unknown)(...args);\n}\n\n/**\n * Resolves a key with the guild's language, as resolved by {@link getSupportedLanguageName}.\n */\nexport function resolveKey<TReturn>(\n interaction: Interaction,\n key: TypedT<TReturn>,\n options?: TOptionsBase | string,\n): TReturn;\nexport function resolveKey<TReturn>(\n interaction: Interaction,\n key: TypedT<TReturn>,\n defaultValue: TReturn,\n options?: TOptionsBase | string,\n): TReturn;\nexport function resolveKey<TArgs extends NonNullObject, TReturn>(\n interaction: Interaction,\n key: TypedFT<TArgs, TReturn>,\n options?: TOptions<TArgs>,\n): TReturn;\nexport function resolveKey<TArgs extends NonNullObject, TReturn>(\n interaction: Interaction,\n key: TypedFT<TArgs, TReturn>,\n defaultValue: TReturn,\n options?: TOptions<TArgs>,\n): TReturn;\nexport function resolveKey(\n interaction: Interaction,\n key: string | string[],\n ...args: [any?, any?]\n): string;\nexport function resolveKey(interaction: Interaction, ...args: [any, any?, any?]) {\n return (getSupportedLanguageT(interaction) as (...args: any[]) => unknown)(...args);\n}\n\nlet cachedLocales: Collection<LocaleString, TFunction> | null = null;\nlet cachedLocalesSize = -1;\n\n/**\n * The loaded languages Discord supports, keyed by locale.\n *\n * @remarks\n * Memoized on the size of `container.i18n.languages` so that locales discovered by a hot reload\n * (see {@link InternationalizationHandler.reloadResources}) are picked up without a restart.\n * @internal\n */\nfunction getLocales(): Collection<LocaleString, TFunction> {\n const { languages } = container.i18n;\n if (cachedLocales && cachedLocalesSize === languages.size) return cachedLocales;\n\n const locales = new Collection<LocaleString, TFunction>();\n\n for (const [locale, t] of languages) {\n if (!isSupportedDiscordLocale(locale)) {\n process.emitWarning(\"Unsupported Discord locale\", {\n code: \"UNSUPPORTED_LOCALE\",\n detail: `'${locale}' is not assignable to type LocaleString`,\n });\n continue;\n }\n\n locales.set(locale, t);\n }\n\n cachedLocales = locales;\n cachedLocalesSize = languages.size;\n return locales;\n}\n\n/**\n * @internal\n */\nfunction getDefaultT(): TFunction {\n const defaultLocale = container.i18n.options.defaultName ?? \"en-US\";\n\n if (!isSupportedDiscordLocale(defaultLocale)) {\n throw new TypeError(\n `Unsupported Discord locale found:\\n'${defaultLocale}' is not within the list of ${[...supportedLanguages]}`,\n );\n }\n\n const defaultT = getLocales().get(defaultLocale);\n if (defaultT) return defaultT;\n throw new TypeError(`Could not find ${defaultLocale}`);\n}\n\n/**\n * Gets the value and the localizations from a language key.\n * @param key The key to get the localizations from.\n * @returns The retrieved data.\n * @remarks This should be called **strictly** after loading the locales.\n */\nexport function getLocalizedData<\n const TOpt extends TOptions = TOptions,\n Ns extends Namespace = DefaultNamespace,\n KPrefix = undefined,\n>(key: ParseKeys<Ns, TOpt, KPrefix> | TypedT): LocalizedData {\n const locales = getLocales();\n const defaultT = getDefaultT();\n\n return {\n value: defaultT(key as never),\n localizations: Object.fromEntries(locales.map((t, locale) => [locale, t(key as never)])),\n };\n}\n\n/**\n * Applies the localized names on the builder, calling `setName` and `setNameLocalizations`.\n * @param builder The builder to apply the localizations to.\n * @param key The key to get the localizations from.\n * @returns The updated builder.\n */\nexport function applyNameLocalizedBuilder<\n T extends BuilderWithName,\n const TOpt extends TOptions = TOptions,\n Ns extends Namespace = DefaultNamespace,\n KPrefix = undefined,\n>(builder: T, key: ParseKeys<Ns, TOpt, KPrefix> | TypedT) {\n const result = getLocalizedData(key);\n return builder.setName(result.value).setNameLocalizations(result.localizations);\n}\n\n/**\n * Applies the localized descriptions on the builder, calling `setDescription` and\n * `setDescriptionLocalizations`.\n * @param builder The builder to apply the localizations to.\n * @param key The key to get the localizations from.\n * @returns The updated builder.\n */\nexport function applyDescriptionLocalizedBuilder<\n T extends BuilderWithDescription,\n const TOpt extends TOptions = TOptions,\n Ns extends Namespace = DefaultNamespace,\n KPrefix = undefined,\n>(builder: T, key: ParseKeys<Ns, TOpt, KPrefix> | TypedT) {\n const result = getLocalizedData(key);\n return builder.setDescription(result.value).setDescriptionLocalizations(result.localizations);\n}\n\n/**\n * Applies the localized names and descriptions on the builder, calling\n * {@link applyNameLocalizedBuilder} and {@link applyDescriptionLocalizedBuilder}.\n *\n * @param builder The builder to apply the localizations to.\n * @param params The root key, or the key for the name and the key for the description.\n * @returns The updated builder. You can chain subsequent builder methods on this.\n *\n * @remarks\n * If only 2 parameters were passed, `name` will be defined as `${root}Name` and `description` as\n * `${root}Description`, being `root` the second parameter in the function, after `builder`.\n *\n * @example\n * ```typescript\n * // Both keys given explicitly:\n * applyLocalizedBuilder(builder, 'commands/names:userinfo', 'commands/descriptions:userinfo');\n * ```\n *\n * @example\n * ```typescript\n * // Root key only, resolves `commands/userinfo:nameName` and `commands/userinfo:nameDescription`:\n * applyLocalizedBuilder(builder, 'commands/userinfo:name');\n * ```\n */\nexport function applyLocalizedBuilder<\n T extends BuilderWithNameAndDescription,\n const TOpt extends TOptions = TOptions,\n Ns extends Namespace = DefaultNamespace,\n KPrefix = undefined,\n>(\n builder: T,\n ...params:\n | [root: LocalePrefixKey]\n | [\n name: ParseKeys<Ns, TOpt, KPrefix> | TypedT,\n description: ParseKeys<Ns, TOpt, KPrefix> | TypedT,\n ]\n): T {\n type LocalKeysType = ParseKeys<Ns, TOpt, KPrefix> | TypedT;\n\n const [localeName, localeDescription] =\n params.length === 1\n ? [`${params[0]}Name` as LocalKeysType, `${params[0]}Description` as LocalKeysType]\n : params;\n\n applyNameLocalizedBuilder(builder, localeName);\n applyDescriptionLocalizedBuilder(builder, localeDescription);\n\n return builder;\n}\n\n/**\n * Constructs an object that can be passed into `setChoices` for a String or Number option with\n * localized names.\n *\n * @param key The i18next key for the name of the choice.\n * @param options The remaining choice options. This should _at least_ include the `value` key.\n * @returns An object with anything provided through `options`, with `name` and `name_localizations`\n * added.\n */\nexport function createLocalizedChoice<\n ValueType = string | number,\n const TOpt extends TOptions = TOptions,\n Ns extends Namespace = DefaultNamespace,\n KPrefix = undefined,\n>(\n key: ParseKeys<Ns, TOpt, KPrefix> | TypedT,\n options: Omit<APIApplicationCommandOptionChoice<ValueType>, \"name\" | \"name_localizations\">,\n): APIApplicationCommandOptionChoice<ValueType> {\n const result = getLocalizedData(key);\n\n return {\n ...options,\n name: result.value,\n name_localizations: result.localizations,\n };\n}\n\n/**\n * Constructs a select menu option with a localized `name`, spreading any extra value on top.\n * @param key The i18next key for the name of the select option.\n * @param value The additional select option properties.\n */\nexport function createSelectMenuChoiceName<V extends NonNullObject>(\n key: TypedT,\n value?: V,\n): createSelectMenuChoiceName.Result<V> {\n const result = getLocalizedData(key);\n return {\n ...value,\n name: result.value,\n name_localizations: result.localizations,\n } as createSelectMenuChoiceName.Result<V>;\n}\n\nexport namespace createSelectMenuChoiceName {\n export type Result<V> = V & {\n name: string;\n name_localizations: import(\"discord-api-types/v10\").LocalizationMap;\n };\n}\n","import { getRootData } from \"@sapphire/pieces\";\nimport { Result } from \"@sapphire/result\";\nimport { isFunction, type Awaitable } from \"@sapphire/utilities\";\nimport { Backend, type PathResolvable } from \"@wolfstar/i18next-backend\";\nimport i18next, {\n type AppendKeyPrefix,\n type DefaultNamespace,\n type InterpolationMap,\n type Namespace,\n type ParseKeys,\n type TFunction,\n type TFunctionProcessReturnValue,\n type TFunctionReturn,\n type TFunctionReturnOptionalDetails,\n type TOptions,\n} from \"i18next\";\nimport type { PathLike } from \"node:fs\";\nimport { opendir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type {\n $Dictionary,\n $NoInfer,\n $SpecialObject,\n InternationalizationContext,\n InternationalizationOptions,\n} from \"./types\";\n\n/**\n * A generalized class for handling `i18next` JSON files and their discovery.\n */\nexport class InternationalizationHandler {\n /**\n * Describes whether {@link InternationalizationHandler.init} has been run and languages are\n * loaded in {@link InternationalizationHandler.languages}.\n */\n public languagesLoaded = false;\n\n /**\n * A `Set` of initially loaded namespaces.\n */\n public namespaces = new Set<string>();\n\n /**\n * A `Map` of `i18next` language functions keyed by their language code.\n */\n public readonly languages = new Map<string, TFunction>();\n\n /**\n * The options {@link InternationalizationHandler} was initialized with.\n */\n public readonly options: InternationalizationOptions;\n\n /**\n * The directory passed to `@wolfstar/i18next-backend`. Also used in\n * {@link InternationalizationHandler.walkRootDirectory}.\n */\n public readonly languagesDirectory: string;\n\n /**\n * The backend options for `@wolfstar/i18next-backend` used by `i18next`.\n */\n protected readonly backendOptions: Backend.Options;\n\n /**\n * The tail of the reload queue, used by {@link reloadResources} to run reloads one at a time.\n */\n private reloadTask: Promise<void> = Promise.resolve();\n\n /**\n * @param options The options that `i18next`, `@wolfstar/i18next-backend`, and\n * {@link InternationalizationHandler} should use.\n */\n public constructor(options?: InternationalizationOptions) {\n this.options = options ?? { i18next: { ignoreJSONStructure: false } };\n this.languagesDirectory =\n this.options.defaultLanguageDirectory ?? join(getRootData().root, \"languages\");\n\n const languagePaths = new Set<PathResolvable>([\n join(this.languagesDirectory, \"{{lng}}\", \"{{ns}}.json\"),\n ...(options?.backend?.paths ?? []),\n ]);\n\n this.backendOptions = {\n paths: [...languagePaths],\n ...this.options.backend,\n };\n\n if (isFunction(this.options.fetchLanguage)) {\n this.fetchLanguage = this.options.fetchLanguage;\n }\n }\n\n /**\n * The method to be overridden by the developer.\n *\n * @remarks\n * In the event that `fetchLanguage` is not defined or returns null / undefined, the interaction's\n * locales are used instead.\n * @returns A string for the desired language or null for no match.\n * @example\n * ```typescript\n * // Always use the same language (no per-guild configuration):\n * container.i18n.fetchLanguage = () => 'en-US';\n * ```\n * @example\n * ```typescript\n * // Retrieving the language from an ORM:\n * container.i18n.fetchLanguage = async (context) => {\n * if (!context.guildId) return null;\n * const guild = await driver.getRepository(GuildEntity).findOne({ id: context.guildId });\n * return guild?.language ?? 'en-US';\n * };\n * ```\n */\n public fetchLanguage: (context: InternationalizationContext) => Awaitable<string | null> = () =>\n null;\n\n /**\n * Initializes the handler by loading in the namespaces, passing the data to i18next, and filling\n * in {@link InternationalizationHandler.languages}.\n */\n public async init() {\n const { namespaces, languages } = await this.walkRootDirectory(this.languagesDirectory);\n const userOptions = isFunction(this.options.i18next)\n ? this.options.i18next(namespaces, languages)\n : this.options.i18next;\n const ignoreJSONStructure = userOptions?.ignoreJSONStructure ?? false;\n const skipOnVariables = userOptions?.interpolation?.skipOnVariables ?? false;\n\n i18next.use(Backend);\n await i18next.init({\n backend: this.backendOptions,\n fallbackLng: this.options.defaultName ?? \"en-US\",\n initImmediate: false,\n interpolation: {\n escapeValue: false,\n ...userOptions?.interpolation,\n skipOnVariables,\n },\n load: \"all\",\n defaultNS: this.options.defaultNS ?? \"default\",\n ns: namespaces,\n preload: languages,\n ...userOptions,\n ignoreJSONStructure,\n });\n\n this.namespaces = new Set(namespaces);\n for (const item of languages) {\n this.languages.set(item, i18next.getFixedT(item));\n }\n this.languagesLoaded = true;\n\n const formatter = i18next.services.formatter!;\n for (const { name, format, cached } of this.options.formatters ?? []) {\n if (cached) formatter.addCached(name, format);\n else formatter.add(name, format);\n }\n }\n\n /**\n * Retrieve a raw `TFunction` from the passed locale.\n * @param locale The language to be used.\n */\n public getT(locale: string) {\n if (!this.languagesLoaded) {\n throw new Error(\n \"Cannot call this method until InternationalizationHandler#init has been called\",\n );\n }\n\n const t = this.languages.get(locale);\n if (t) return t;\n throw new ReferenceError(`Invalid language (${locale})`);\n }\n\n /**\n * Localizes a content given one or more keys and i18next options.\n * @param locale The language to be used.\n * @param key The key or keys to retrieve the content from.\n * @param options The interpolation options.\n * @see {@link https://www.i18next.com/overview/api#t}\n * @returns The localized content.\n */\n public format<\n const Key extends ParseKeys<Ns, TOpt, undefined>,\n const TOpt extends TOptions = TOptions,\n Ns extends Namespace = DefaultNamespace,\n Ret extends TFunctionReturn<Ns, AppendKeyPrefix<Key, undefined>, TOpt> =\n TOpt[\"returnObjects\"] extends true ? $SpecialObject : string,\n const ActualOptions extends TOpt & InterpolationMap<Ret> = TOpt & InterpolationMap<Ret>,\n >(\n locale: string,\n key: Key | Key[],\n options?: ActualOptions,\n ): TFunctionReturnOptionalDetails<Ret, TOpt>;\n\n /**\n * Localizes a content given one or more keys and i18next options.\n * @param locale The language to be used.\n * @param key The key or keys to retrieve the content from.\n * @param options The interpolation options as well as a `defaultValue` for the key and any\n * key/value pairs.\n * @see {@link https://www.i18next.com/overview/api#t}\n * @returns The localized content.\n */\n public format<\n const Key extends ParseKeys<Ns, TOpt, undefined>,\n const TOpt extends TOptions = TOptions,\n Ns extends Namespace = DefaultNamespace,\n Ret extends TFunctionReturn<Ns, AppendKeyPrefix<Key, undefined>, TOpt> =\n TOpt[\"returnObjects\"] extends true ? $SpecialObject : string,\n >(\n locale: string,\n key: string | string[],\n options: TOpt & $Dictionary & { defaultValue: string },\n ): TFunctionReturnOptionalDetails<Ret, TOpt>;\n\n /**\n * Localizes a content given one or more keys and i18next options.\n * @param locale The language to be used.\n * @param key The key or keys to retrieve the content from.\n * @param defaultValue The default value to use if the key is not found.\n * @param options The interpolation options.\n * @see {@link https://www.i18next.com/overview/api#t}\n * @returns The localized content.\n */\n public format<\n const Key extends ParseKeys<Ns, TOpt, undefined>,\n const TOpt extends TOptions = TOptions,\n Ns extends Namespace = DefaultNamespace,\n Ret extends TFunctionReturn<Ns, AppendKeyPrefix<Key, undefined>, TOpt> =\n TOpt[\"returnObjects\"] extends true ? $SpecialObject : string,\n >(\n locale: string,\n key: string | string[],\n defaultValue: string | undefined,\n options?: TOpt & $Dictionary,\n ): TFunctionReturnOptionalDetails<Ret, TOpt>;\n\n /**\n * Localizes a content given one or more keys and i18next options.\n * @param locale The language to be used.\n *\n * @remarks\n * This function also has additional parameters for `key`, `defaultValue`, and `options`, however\n * TSDoc does not let us document those while matching the implementation signature. See the\n * overloads for this method for the documentation on those parameters.\n *\n * @see {@link https://www.i18next.com/overview/api#t}\n * @returns The localized content.\n */\n public format<\n const Key extends ParseKeys<Ns, TOpt, undefined>,\n const TOpt extends TOptions = TOptions,\n Ns extends Namespace = DefaultNamespace,\n Ret extends TFunctionReturn<Ns, AppendKeyPrefix<Key, undefined>, TOpt> =\n TOpt[\"returnObjects\"] extends true ? $SpecialObject : string,\n const ActualOptions extends TOpt & InterpolationMap<Ret> = TOpt & InterpolationMap<Ret>,\n DefaultValue extends string = never,\n >(\n locale: string,\n ...[key, defaultValueOrOptions, optionsOrUndefined]:\n | [key: Key | Key[], options?: ActualOptions]\n | [key: string | string[], options: TOpt & $Dictionary & { defaultValue: string }]\n | [\n key: string | string[],\n defaultValue: DefaultValue | undefined,\n options?: TOpt & $Dictionary,\n ]\n ): TFunctionReturnOptionalDetails<\n TFunctionProcessReturnValue<$NoInfer<Ret>, DefaultValue>,\n TOpt\n > {\n const language = this.getT(locale);\n\n // `defaultValueOrOptions` holds the default value only when it is a string; otherwise it holds\n // the options object and `optionsOrUndefined` is not provided.\n const hasDefaultValue = typeof defaultValueOrOptions === \"string\";\n const options = (hasDefaultValue ? optionsOrUndefined : defaultValueOrOptions) ?? {};\n const defaultValue = hasDefaultValue\n ? defaultValueOrOptions\n : this.options.defaultMissingKey\n ? language(this.options.defaultMissingKey, { replace: { key } })\n : \"\";\n\n return language(\n key as never,\n {\n defaultValue,\n ...(options as TOpt),\n } as never,\n ) as TFunctionReturnOptionalDetails<\n TFunctionProcessReturnValue<$NoInfer<Ret>, DefaultValue>,\n TOpt\n >;\n }\n\n /**\n * Walks the root languages directory, collecting every language and namespace found in it.\n * @param directory The directory that should be walked.\n */\n public async walkRootDirectory(directory: PathLike) {\n const languages = new Set<string>();\n const namespaces = new Set<string>();\n\n const dir = await opendir(directory);\n for await (const entry of dir) {\n // If the entry is not a directory, skip:\n if (!entry.isDirectory()) continue;\n\n // Load the directory:\n languages.add(entry.name);\n\n for await (const namespace of this.walkLocaleDirectory(join(dir.path, entry.name), \"\")) {\n namespaces.add(namespace);\n }\n }\n\n return { namespaces: [...namespaces], languages: [...languages] };\n }\n\n /**\n * Reloads the languages and namespaces registered in i18next, used by the HMR watcher registered\n * in `@wolfstar/plugin-i18next/register`.\n */\n public reloadResources(): Promise<void> {\n // Serialize reloads: the watcher can fire several events for a single edit (`addDir` followed by\n // `add`, for instance), and overlapping `i18next.loadLanguages` calls would race each other.\n this.reloadTask = this.reloadTask.then(() => this.performReload());\n return this.reloadTask;\n }\n\n /**\n * Registers the languages and namespaces that were discovered after {@link init} ran, so locales\n * and namespaces added while the process is running become usable without a restart.\n * @param discovered The result of {@link walkRootDirectory}.\n */\n private async registerDiscoveredResources(discovered: {\n languages: string[];\n namespaces: string[];\n }) {\n const newNamespaces = discovered.namespaces.filter(\n (namespace) => !this.namespaces.has(namespace),\n );\n if (newNamespaces.length > 0) {\n await i18next.loadNamespaces(newNamespaces);\n for (const namespace of newNamespaces) this.namespaces.add(namespace);\n }\n\n const newLanguages = discovered.languages.filter((language) => !this.languages.has(language));\n if (newLanguages.length > 0) {\n await i18next.loadLanguages(newLanguages);\n for (const language of newLanguages)\n this.languages.set(language, i18next.getFixedT(language));\n }\n }\n\n /**\n * @internal\n */\n private async performReload() {\n const result = await Result.fromAsync(async () => {\n let languages = this.options.hmr?.languages;\n let namespaces = this.options.hmr?.namespaces;\n\n // Only walk the directory when at least one of the two was not pinned through the options,\n // matching what `init` discovers, and register anything that appeared since.\n if (!languages || !namespaces) {\n const languageDirectoryResult = await this.walkRootDirectory(this.languagesDirectory);\n await this.registerDiscoveredResources(languageDirectoryResult);\n languages ??= languageDirectoryResult.languages;\n namespaces ??= languageDirectoryResult.namespaces;\n }\n\n await i18next.reloadResources(languages, namespaces);\n console.info(\"[plugin-i18next] Reloaded language resources.\");\n });\n\n result.inspectErr((error: unknown) =>\n console.error(\"[plugin-i18next] Failed to reload language resources.\", error),\n );\n }\n\n /**\n * Walks a single locale directory, yielding every namespace found in it.\n *\n * @remarks\n * Skips any file that does not end with `.json`.\n * @param directory The directory that should be walked.\n * @param ns The current namespace.\n */\n private async *walkLocaleDirectory(directory: string, ns: string): AsyncGenerator<string> {\n const dir = await opendir(directory);\n for await (const entry of dir) {\n if (entry.isDirectory()) {\n yield* this.walkLocaleDirectory(join(dir.path, entry.name), `${ns}${entry.name}/`);\n } else if (entry.isFile() && entry.name.endsWith(\".json\")) {\n yield `${ns}${entry.name.slice(0, -5)}`;\n }\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["i18next"],"sources":["../../src/lib/functions.ts","../../src/lib/InternationalizationHandler.ts"],"sourcesContent":["import { Collection } from \"@discordjs/collection\";\nimport { container } from \"@sapphire/pieces\";\nimport type { NonNullObject } from \"@sapphire/utilities\";\nimport {\n Locale,\n type APIApplicationCommandOptionChoice,\n type LocaleString,\n} from \"discord-api-types/v10\";\nimport type {\n AppendKeyPrefix,\n DefaultNamespace,\n InterpolationMap,\n Namespace,\n ParseKeys,\n TFunction,\n TFunctionReturn,\n TFunctionReturnOptionalDetails,\n TOptions,\n TOptionsBase,\n} from \"i18next\";\nimport type {\n $Dictionary,\n $SpecialObject,\n BuilderWithDescription,\n BuilderWithName,\n BuilderWithNameAndDescription,\n InternationalizationContext,\n LocalePrefixKey,\n LocalizedData,\n Target,\n TypedFT,\n TypedT,\n} from \"./types\";\n\n/**\n * Brands a translation key with the type it resolves to.\n * @param k The i18next key.\n * @example\n * ```typescript\n * export const InvalidInput = T('path/to/file:invalidInput');\n * ```\n */\nexport function T<TCustom = string>(k: string): TypedT<TCustom> {\n return k as TypedT<TCustom>;\n}\n\n/**\n * Brands a translation key with both its interpolation arguments and the type it resolves to.\n * @param k The i18next key.\n * @example\n * ```typescript\n * export const AddResult = FT<{ left: number; right: number; result: number }>('path/to/file:addResult');\n * ```\n */\nexport function FT<TArgs extends NonNullObject = NonNullObject, TReturn = string>(\n k: string,\n): TypedFT<TArgs, TReturn> {\n return k as TypedFT<TArgs, TReturn>;\n}\n\n/**\n * Every locale Discord supports.\n */\nexport const supportedLanguages = new Set(Object.values(Locale)) as ReadonlySet<LocaleString>;\n\n/**\n * Checks whether the given language is a locale Discord supports.\n * @param language The language to check.\n */\nexport function isSupportedDiscordLocale(language: string): language is LocaleString {\n return supportedLanguages.has(language as LocaleString);\n}\n\n/**\n * Resolves the fallback language to use when none of a target's locales is loaded.\n *\n * {@link InternationalizationOptions.defaultName} wins as long as it is both a Discord locale and a\n * loaded language; otherwise `'en-US'` is used.\n * @internal\n */\nfunction getFallbackLanguageName(): LocaleString {\n const { languages, options } = container.i18n;\n const { defaultName } = options;\n\n return defaultName && isSupportedDiscordLocale(defaultName) && languages.has(defaultName)\n ? defaultName\n : \"en-US\";\n}\n\n/**\n * Narrows a locale to a loaded Discord locale, or `null` when it is neither.\n * @internal\n */\nfunction getLoadedLocale(locale: string | undefined): LocaleString | null {\n if (!locale) return null;\n return isSupportedDiscordLocale(locale) && container.i18n.languages.has(locale) ? locale : null;\n}\n\n/**\n * Builds the {@link InternationalizationContext} for any supported target.\n *\n * @remarks\n * The members of {@link Target} are told apart structurally, since the framework receives raw\n * payloads rather than class instances: an interaction has `locale`, a message has `channel_id`, a\n * channel has `type`, and anything left is a guild.\n * @internal\n */\nfunction resolveContext(target: Target): InternationalizationContext {\n if (\"locale\" in target) {\n return {\n guildId: target.guild_id ?? null,\n channelId: target.channel_id ?? null,\n userId: target.user?.id ?? target.member?.user.id ?? null,\n interactionGuildLocale: target.guild_locale,\n interactionLocale: target.locale,\n preferredLocale: target.guild_locale,\n };\n }\n\n if (\"channel_id\" in target) {\n return {\n guildId: target.guild_id ?? null,\n channelId: target.channel_id,\n userId: target.author?.id ?? null,\n };\n }\n\n if (\"type\" in target) {\n return {\n guildId: target.guild_id ?? null,\n channelId: target.id,\n userId: null,\n };\n }\n\n return {\n guildId: target.id,\n channelId: null,\n userId: null,\n preferredLocale: target.preferred_locale,\n };\n}\n\n/**\n * @internal\n */\nfunction getSupportedLanguageNameFromContext(context: InternationalizationContext): LocaleString {\n // Guild-scoped: the guild's locale is what matters, and the user's is deliberately ignored.\n // Outside a guild there is no guild locale to prefer, so the user's one is used instead.\n const preferred = context.guildId\n ? getLoadedLocale(context.preferredLocale)\n : getLoadedLocale(context.interactionLocale);\n\n return preferred ?? getFallbackLanguageName();\n}\n\n/**\n * @internal\n */\nfunction getSupportedUserLanguageNameFromContext(\n context: InternationalizationContext,\n): LocaleString {\n return (\n getLoadedLocale(context.interactionLocale) ??\n getLoadedLocale(context.preferredLocale) ??\n getFallbackLanguageName()\n );\n}\n\n/**\n * Resolves the loaded language that best matches the user's locale, falling back to the guild's one,\n * then to {@link InternationalizationOptions.defaultName}, and finally to `'en-US'`.\n *\n * @remarks\n * Only an {@link Interaction} carries a user locale. For a {@link GuildTarget} this is equivalent to\n * {@link getSupportedLanguageName}, and a {@link ChannelTarget} or {@link MessageTarget} always\n * resolves to the fallback.\n * @param target The target to read the locales from.\n */\nexport function getSupportedUserLanguageName(target: Target): LocaleString {\n return getSupportedUserLanguageNameFromContext(resolveContext(target));\n}\n\n/**\n * Resolves the `TFunction` for {@link getSupportedUserLanguageName}.\n * @param target The target to read the locales from.\n */\nexport function getSupportedUserLanguageT(target: Target): TFunction {\n return container.i18n.getT(getSupportedUserLanguageName(target));\n}\n\n/**\n * Resolves the loaded language that best matches the guild's locale, falling back to the user's one\n * when the target does not belong to a guild, then to\n * {@link InternationalizationOptions.defaultName}, and finally to `'en-US'`.\n *\n * @remarks\n * The guild locale comes from `guild_locale` on an {@link Interaction} and from `preferred_locale`\n * on a {@link GuildTarget}. A {@link ChannelTarget} and a {@link MessageTarget} carry no locale, so\n * they resolve to the fallback unless a custom\n * {@link InternationalizationHandler.fetchLanguage} hook is used through {@link fetchLanguage}.\n * @param target The target to read the locales from.\n */\nexport function getSupportedLanguageName(target: Target): LocaleString {\n return getSupportedLanguageNameFromContext(resolveContext(target));\n}\n\n/**\n * Resolves the `TFunction` for {@link getSupportedLanguageName}.\n * @param target The target to read the locales from.\n */\nexport function getSupportedLanguageT(target: Target): TFunction {\n return container.i18n.getT(getSupportedLanguageName(target));\n}\n\n/**\n * Retrieves the language name for a target, using {@link InternationalizationHandler.fetchLanguage}.\n *\n * If that hook is not defined or returns a nullish value, the language is resolved from the\n * locales the target carries through {@link getSupportedLanguageName}, which itself falls back to\n * {@link InternationalizationOptions.defaultName} and then to `'en-US'`.\n\n * @remarks\n * This is the only helper that can resolve a language for a {@link ChannelTarget} or a\n * {@link MessageTarget}, since those payloads carry no locale and the hook receives their\n * `guildId` and `channelId`.\n * @param target The target to fetch the language from.\n */\nexport async function fetchLanguage(target: Target): Promise<string> {\n const context = resolveContext(target);\n const language = await container.i18n.fetchLanguage(context);\n return language ?? getSupportedLanguageNameFromContext(context);\n}\n\n/**\n * Retrieves the language-assigned function from i18next designated to a target's preferred language.\n * @param target The target to fetch the language from.\n */\nexport async function fetchT(target: Target): Promise<TFunction> {\n return container.i18n.getT(await fetchLanguage(target));\n}\n\n/**\n * Resolves a key and its parameters using {@link fetchLanguage}, meaning a custom\n * {@link InternationalizationHandler.fetchLanguage} hook (for example, a per-guild database lookup)\n * is honoured.\n *\n * @remarks\n * Use {@link resolveKey} when the language can be resolved from the target payload alone, it is\n * synchronous and does not hit the hook.\n * @param target The target to fetch the language key from.\n */\nexport async function fetchKey<\n const Key extends ParseKeys<Ns, TOpt, undefined>,\n const TOpt extends TOptions = TOptions,\n Ns extends Namespace = DefaultNamespace,\n Ret extends TFunctionReturn<Ns, AppendKeyPrefix<Key, undefined>, TOpt> =\n TOpt[\"returnObjects\"] extends true ? $SpecialObject : string,\n const ActualOptions extends TOpt & InterpolationMap<Ret> = TOpt & InterpolationMap<Ret>,\n>(\n target: Target,\n ...[key, defaultValueOrOptions, optionsOrUndefined]:\n | [key: Key | Key[], options?: ActualOptions]\n | [key: string | string[], options: TOpt & $Dictionary & { defaultValue: string }]\n | [key: string | string[], defaultValue: string, options?: TOpt & $Dictionary]\n): Promise<TFunctionReturnOptionalDetails<Ret, TOpt>> {\n const parsedOptions =\n typeof defaultValueOrOptions === \"string\" ? optionsOrUndefined : defaultValueOrOptions;\n const language =\n typeof parsedOptions?.lng === \"string\" ? parsedOptions.lng : await fetchLanguage(target);\n\n if (typeof defaultValueOrOptions === \"string\") {\n return container.i18n.format<Key, TOpt, Ns, Ret>(\n language,\n key,\n defaultValueOrOptions,\n optionsOrUndefined,\n );\n }\n\n return container.i18n.format<Key, TOpt, Ns, Ret>(language, key, undefined, defaultValueOrOptions);\n}\n\n/**\n * Resolves a key with the user's language, as resolved by {@link getSupportedUserLanguageName}.\n */\nexport function resolveUserKey<TReturn>(\n target: Target,\n key: TypedT<TReturn>,\n options?: TOptionsBase | string,\n): TReturn;\nexport function resolveUserKey<TReturn>(\n target: Target,\n key: TypedT<TReturn>,\n defaultValue: TReturn,\n options?: TOptionsBase | string,\n): TReturn;\nexport function resolveUserKey<TArgs extends NonNullObject, TReturn>(\n target: Target,\n key: TypedFT<TArgs, TReturn>,\n options?: TOptions<TArgs>,\n): TReturn;\nexport function resolveUserKey<TArgs extends NonNullObject, TReturn>(\n target: Target,\n key: TypedFT<TArgs, TReturn>,\n defaultValue: TReturn,\n options?: TOptions<TArgs>,\n): TReturn;\nexport function resolveUserKey(\n target: Target,\n key: string | string[],\n ...args: [any?, any?]\n): string;\nexport function resolveUserKey(target: Target, ...args: [any, any?, any?]) {\n return (getSupportedUserLanguageT(target) as (...args: any[]) => unknown)(...args);\n}\n\n/**\n * Resolves a key with the guild's language, as resolved by {@link getSupportedLanguageName}.\n */\nexport function resolveKey<TReturn>(\n target: Target,\n key: TypedT<TReturn>,\n options?: TOptionsBase | string,\n): TReturn;\nexport function resolveKey<TReturn>(\n target: Target,\n key: TypedT<TReturn>,\n defaultValue: TReturn,\n options?: TOptionsBase | string,\n): TReturn;\nexport function resolveKey<TArgs extends NonNullObject, TReturn>(\n target: Target,\n key: TypedFT<TArgs, TReturn>,\n options?: TOptions<TArgs>,\n): TReturn;\nexport function resolveKey<TArgs extends NonNullObject, TReturn>(\n target: Target,\n key: TypedFT<TArgs, TReturn>,\n defaultValue: TReturn,\n options?: TOptions<TArgs>,\n): TReturn;\nexport function resolveKey(target: Target, key: string | string[], ...args: [any?, any?]): string;\nexport function resolveKey(target: Target, ...args: [any, any?, any?]) {\n return (getSupportedLanguageT(target) as (...args: any[]) => unknown)(...args);\n}\n\nlet cachedLocales: Collection<LocaleString, TFunction> | null = null;\nlet cachedLocalesSize = -1;\n\n/**\n * The loaded languages Discord supports, keyed by locale.\n *\n * @remarks\n * Memoized on the size of `container.i18n.languages` so that locales discovered by a hot reload\n * (see {@link InternationalizationHandler.reloadResources}) are picked up without a restart.\n * @internal\n */\nfunction getLocales(): Collection<LocaleString, TFunction> {\n const { languages } = container.i18n;\n if (cachedLocales && cachedLocalesSize === languages.size) return cachedLocales;\n\n const locales = new Collection<LocaleString, TFunction>();\n\n for (const [locale, t] of languages) {\n if (!isSupportedDiscordLocale(locale)) {\n process.emitWarning(\"Unsupported Discord locale\", {\n code: \"UNSUPPORTED_LOCALE\",\n detail: `'${locale}' is not assignable to type LocaleString`,\n });\n continue;\n }\n\n locales.set(locale, t);\n }\n\n cachedLocales = locales;\n cachedLocalesSize = languages.size;\n return locales;\n}\n\n/**\n * @internal\n */\nfunction getDefaultT(): TFunction {\n const defaultLocale = container.i18n.options.defaultName ?? \"en-US\";\n\n if (!isSupportedDiscordLocale(defaultLocale)) {\n throw new TypeError(\n `Unsupported Discord locale found:\\n'${defaultLocale}' is not within the list of ${[...supportedLanguages]}`,\n );\n }\n\n const defaultT = getLocales().get(defaultLocale);\n if (defaultT) return defaultT;\n throw new TypeError(`Could not find ${defaultLocale}`);\n}\n\n/**\n * Gets the value and the localizations from a language key.\n * @param key The key to get the localizations from.\n * @returns The retrieved data.\n * @remarks This should be called **strictly** after loading the locales.\n */\nexport function getLocalizedData<\n const TOpt extends TOptions = TOptions,\n Ns extends Namespace = DefaultNamespace,\n KPrefix = undefined,\n>(key: ParseKeys<Ns, TOpt, KPrefix> | TypedT): LocalizedData {\n const locales = getLocales();\n const defaultT = getDefaultT();\n\n return {\n value: defaultT(key as never),\n localizations: Object.fromEntries(locales.map((t, locale) => [locale, t(key as never)])),\n };\n}\n\n/**\n * Applies the localized names on the builder, calling `setName` and `setNameLocalizations`.\n * @param builder The builder to apply the localizations to.\n * @param key The key to get the localizations from.\n * @returns The updated builder.\n */\nexport function applyNameLocalizedBuilder<\n T extends BuilderWithName,\n const TOpt extends TOptions = TOptions,\n Ns extends Namespace = DefaultNamespace,\n KPrefix = undefined,\n>(builder: T, key: ParseKeys<Ns, TOpt, KPrefix> | TypedT) {\n const result = getLocalizedData(key);\n return builder.setName(result.value).setNameLocalizations(result.localizations);\n}\n\n/**\n * Applies the localized descriptions on the builder, calling `setDescription` and\n * `setDescriptionLocalizations`.\n * @param builder The builder to apply the localizations to.\n * @param key The key to get the localizations from.\n * @returns The updated builder.\n */\nexport function applyDescriptionLocalizedBuilder<\n T extends BuilderWithDescription,\n const TOpt extends TOptions = TOptions,\n Ns extends Namespace = DefaultNamespace,\n KPrefix = undefined,\n>(builder: T, key: ParseKeys<Ns, TOpt, KPrefix> | TypedT) {\n const result = getLocalizedData(key);\n return builder.setDescription(result.value).setDescriptionLocalizations(result.localizations);\n}\n\n/**\n * Applies the localized names and descriptions on the builder, calling\n * {@link applyNameLocalizedBuilder} and {@link applyDescriptionLocalizedBuilder}.\n *\n * @param builder The builder to apply the localizations to.\n * @param params The root key, or the key for the name and the key for the description.\n * @returns The updated builder. You can chain subsequent builder methods on this.\n *\n * @remarks\n * If only 2 parameters were passed, `name` will be defined as `${root}Name` and `description` as\n * `${root}Description`, being `root` the second parameter in the function, after `builder`.\n *\n * @example\n * ```typescript\n * // Both keys given explicitly:\n * applyLocalizedBuilder(builder, 'commands/names:userinfo', 'commands/descriptions:userinfo');\n * ```\n *\n * @example\n * ```typescript\n * // Root key only, resolves `commands/userinfo:nameName` and `commands/userinfo:nameDescription`:\n * applyLocalizedBuilder(builder, 'commands/userinfo:name');\n * ```\n */\nexport function applyLocalizedBuilder<\n T extends BuilderWithNameAndDescription,\n const TOpt extends TOptions = TOptions,\n Ns extends Namespace = DefaultNamespace,\n KPrefix = undefined,\n>(\n builder: T,\n ...params:\n | [root: LocalePrefixKey]\n | [\n name: ParseKeys<Ns, TOpt, KPrefix> | TypedT,\n description: ParseKeys<Ns, TOpt, KPrefix> | TypedT,\n ]\n): T {\n type LocalKeysType = ParseKeys<Ns, TOpt, KPrefix> | TypedT;\n\n const [localeName, localeDescription] =\n params.length === 1\n ? [`${params[0]}Name` as LocalKeysType, `${params[0]}Description` as LocalKeysType]\n : params;\n\n applyNameLocalizedBuilder(builder, localeName);\n applyDescriptionLocalizedBuilder(builder, localeDescription);\n\n return builder;\n}\n\n/**\n * Constructs an object that can be passed into `setChoices` for a String or Number option with\n * localized names.\n *\n * @param key The i18next key for the name of the choice.\n * @param options The remaining choice options. This should _at least_ include the `value` key.\n * @returns An object with anything provided through `options`, with `name` and `name_localizations`\n * added.\n */\nexport function createLocalizedChoice<\n ValueType = string | number,\n const TOpt extends TOptions = TOptions,\n Ns extends Namespace = DefaultNamespace,\n KPrefix = undefined,\n>(\n key: ParseKeys<Ns, TOpt, KPrefix> | TypedT,\n options: Omit<APIApplicationCommandOptionChoice<ValueType>, \"name\" | \"name_localizations\">,\n): APIApplicationCommandOptionChoice<ValueType> {\n const result = getLocalizedData(key);\n\n return {\n ...options,\n name: result.value,\n name_localizations: result.localizations,\n };\n}\n\n/**\n * Constructs a select menu option with a localized `name`, spreading any extra value on top.\n * @param key The i18next key for the name of the select option.\n * @param value The additional select option properties.\n */\nexport function createSelectMenuChoiceName<V extends NonNullObject>(\n key: TypedT,\n value?: V,\n): createSelectMenuChoiceName.Result<V> {\n const result = getLocalizedData(key);\n return {\n ...value,\n name: result.value,\n name_localizations: result.localizations,\n } as createSelectMenuChoiceName.Result<V>;\n}\n\nexport namespace createSelectMenuChoiceName {\n export type Result<V> = V & {\n name: string;\n name_localizations: import(\"discord-api-types/v10\").LocalizationMap;\n };\n}\n","import { getRootData } from \"@sapphire/pieces\";\nimport { Result } from \"@sapphire/result\";\nimport { isFunction, type Awaitable } from \"@sapphire/utilities\";\nimport { Backend, type PathResolvable } from \"@wolfstar/i18next-backend\";\nimport i18next, {\n type AppendKeyPrefix,\n type DefaultNamespace,\n type InterpolationMap,\n type Namespace,\n type ParseKeys,\n type TFunction,\n type TFunctionProcessReturnValue,\n type TFunctionReturn,\n type TFunctionReturnOptionalDetails,\n type TOptions,\n} from \"i18next\";\nimport type { PathLike } from \"node:fs\";\nimport { opendir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type {\n $Dictionary,\n $NoInfer,\n $SpecialObject,\n InternationalizationContext,\n InternationalizationOptions,\n} from \"./types\";\n\n/**\n * A generalized class for handling `i18next` JSON files and their discovery.\n */\nexport class InternationalizationHandler {\n /**\n * Describes whether {@link InternationalizationHandler.init} has been run and languages are\n * loaded in {@link InternationalizationHandler.languages}.\n */\n public languagesLoaded = false;\n\n /**\n * A `Set` of initially loaded namespaces.\n */\n public namespaces = new Set<string>();\n\n /**\n * A `Map` of `i18next` language functions keyed by their language code.\n */\n public readonly languages = new Map<string, TFunction>();\n\n /**\n * The options {@link InternationalizationHandler} was initialized with.\n */\n public readonly options: InternationalizationOptions;\n\n /**\n * The directory passed to `@wolfstar/i18next-backend`. Also used in\n * {@link InternationalizationHandler.walkRootDirectory}.\n */\n public readonly languagesDirectory: string;\n\n /**\n * The backend options for `@wolfstar/i18next-backend` used by `i18next`.\n */\n protected readonly backendOptions: Backend.Options;\n\n /**\n * The tail of the reload queue, used by {@link reloadResources} to run reloads one at a time.\n */\n private reloadTask: Promise<void> = Promise.resolve();\n\n /**\n * @param options The options that `i18next`, `@wolfstar/i18next-backend`, and\n * {@link InternationalizationHandler} should use.\n */\n public constructor(options?: InternationalizationOptions) {\n this.options = options ?? { i18next: { ignoreJSONStructure: false } };\n this.languagesDirectory =\n this.options.defaultLanguageDirectory ?? join(getRootData().root, \"languages\");\n\n const languagePaths = new Set<PathResolvable>([\n join(this.languagesDirectory, \"{{lng}}\", \"{{ns}}.json\"),\n ...(options?.backend?.paths ?? []),\n ]);\n\n this.backendOptions = {\n paths: [...languagePaths],\n ...this.options.backend,\n };\n\n if (isFunction(this.options.fetchLanguage)) {\n this.fetchLanguage = this.options.fetchLanguage;\n }\n }\n\n /**\n * The method to be overridden by the developer.\n *\n * @remarks\n * In the event that `fetchLanguage` is not defined or returns null / undefined, the interaction's\n * locales are used instead.\n * @returns A string for the desired language or null for no match.\n * @example\n * ```typescript\n * // Always use the same language (no per-guild configuration):\n * container.i18n.fetchLanguage = () => 'en-US';\n * ```\n * @example\n * ```typescript\n * // Retrieving the language from an ORM:\n * container.i18n.fetchLanguage = async (context) => {\n * if (!context.guildId) return null;\n * const guild = await driver.getRepository(GuildEntity).findOne({ id: context.guildId });\n * return guild?.language ?? 'en-US';\n * };\n * ```\n */\n public fetchLanguage: (context: InternationalizationContext) => Awaitable<string | null> = () =>\n null;\n\n /**\n * Initializes the handler by loading in the namespaces, passing the data to i18next, and filling\n * in {@link InternationalizationHandler.languages}.\n */\n public async init() {\n const { namespaces, languages } = await this.walkRootDirectory(this.languagesDirectory);\n const userOptions = isFunction(this.options.i18next)\n ? this.options.i18next(namespaces, languages)\n : this.options.i18next;\n const ignoreJSONStructure = userOptions?.ignoreJSONStructure ?? false;\n const skipOnVariables = userOptions?.interpolation?.skipOnVariables ?? false;\n\n i18next.use(Backend);\n await i18next.init({\n backend: this.backendOptions,\n fallbackLng: this.options.defaultName ?? \"en-US\",\n initImmediate: false,\n interpolation: {\n escapeValue: false,\n ...userOptions?.interpolation,\n skipOnVariables,\n },\n load: \"all\",\n defaultNS: this.options.defaultNS ?? \"default\",\n ns: namespaces,\n preload: languages,\n ...userOptions,\n ignoreJSONStructure,\n });\n\n this.namespaces = new Set(namespaces);\n for (const item of languages) {\n this.languages.set(item, i18next.getFixedT(item));\n }\n this.languagesLoaded = true;\n\n const formatter = i18next.services.formatter!;\n for (const { name, format, cached } of this.options.formatters ?? []) {\n if (cached) formatter.addCached(name, format);\n else formatter.add(name, format);\n }\n }\n\n /**\n * Retrieve a raw `TFunction` from the passed locale.\n * @param locale The language to be used.\n */\n public getT(locale: string) {\n if (!this.languagesLoaded) {\n throw new Error(\n \"Cannot call this method until InternationalizationHandler#init has been called\",\n );\n }\n\n const t = this.languages.get(locale);\n if (t) return t;\n throw new ReferenceError(`Invalid language (${locale})`);\n }\n\n /**\n * Localizes a content given one or more keys and i18next options.\n * @param locale The language to be used.\n * @param key The key or keys to retrieve the content from.\n * @param options The interpolation options.\n * @see {@link https://www.i18next.com/overview/api#t}\n * @returns The localized content.\n */\n public format<\n const Key extends ParseKeys<Ns, TOpt, undefined>,\n const TOpt extends TOptions = TOptions,\n Ns extends Namespace = DefaultNamespace,\n Ret extends TFunctionReturn<Ns, AppendKeyPrefix<Key, undefined>, TOpt> =\n TOpt[\"returnObjects\"] extends true ? $SpecialObject : string,\n const ActualOptions extends TOpt & InterpolationMap<Ret> = TOpt & InterpolationMap<Ret>,\n >(\n locale: string,\n key: Key | Key[],\n options?: ActualOptions,\n ): TFunctionReturnOptionalDetails<Ret, TOpt>;\n\n /**\n * Localizes a content given one or more keys and i18next options.\n * @param locale The language to be used.\n * @param key The key or keys to retrieve the content from.\n * @param options The interpolation options as well as a `defaultValue` for the key and any\n * key/value pairs.\n * @see {@link https://www.i18next.com/overview/api#t}\n * @returns The localized content.\n */\n public format<\n const Key extends ParseKeys<Ns, TOpt, undefined>,\n const TOpt extends TOptions = TOptions,\n Ns extends Namespace = DefaultNamespace,\n Ret extends TFunctionReturn<Ns, AppendKeyPrefix<Key, undefined>, TOpt> =\n TOpt[\"returnObjects\"] extends true ? $SpecialObject : string,\n >(\n locale: string,\n key: string | string[],\n options: TOpt & $Dictionary & { defaultValue: string },\n ): TFunctionReturnOptionalDetails<Ret, TOpt>;\n\n /**\n * Localizes a content given one or more keys and i18next options.\n * @param locale The language to be used.\n * @param key The key or keys to retrieve the content from.\n * @param defaultValue The default value to use if the key is not found.\n * @param options The interpolation options.\n * @see {@link https://www.i18next.com/overview/api#t}\n * @returns The localized content.\n */\n public format<\n const Key extends ParseKeys<Ns, TOpt, undefined>,\n const TOpt extends TOptions = TOptions,\n Ns extends Namespace = DefaultNamespace,\n Ret extends TFunctionReturn<Ns, AppendKeyPrefix<Key, undefined>, TOpt> =\n TOpt[\"returnObjects\"] extends true ? $SpecialObject : string,\n >(\n locale: string,\n key: string | string[],\n defaultValue: string | undefined,\n options?: TOpt & $Dictionary,\n ): TFunctionReturnOptionalDetails<Ret, TOpt>;\n\n /**\n * Localizes a content given one or more keys and i18next options.\n * @param locale The language to be used.\n *\n * @remarks\n * This function also has additional parameters for `key`, `defaultValue`, and `options`, however\n * TSDoc does not let us document those while matching the implementation signature. See the\n * overloads for this method for the documentation on those parameters.\n *\n * @see {@link https://www.i18next.com/overview/api#t}\n * @returns The localized content.\n */\n public format<\n const Key extends ParseKeys<Ns, TOpt, undefined>,\n const TOpt extends TOptions = TOptions,\n Ns extends Namespace = DefaultNamespace,\n Ret extends TFunctionReturn<Ns, AppendKeyPrefix<Key, undefined>, TOpt> =\n TOpt[\"returnObjects\"] extends true ? $SpecialObject : string,\n const ActualOptions extends TOpt & InterpolationMap<Ret> = TOpt & InterpolationMap<Ret>,\n DefaultValue extends string = never,\n >(\n locale: string,\n ...[key, defaultValueOrOptions, optionsOrUndefined]:\n | [key: Key | Key[], options?: ActualOptions]\n | [key: string | string[], options: TOpt & $Dictionary & { defaultValue: string }]\n | [\n key: string | string[],\n defaultValue: DefaultValue | undefined,\n options?: TOpt & $Dictionary,\n ]\n ): TFunctionReturnOptionalDetails<\n TFunctionProcessReturnValue<$NoInfer<Ret>, DefaultValue>,\n TOpt\n > {\n const language = this.getT(locale);\n\n // `defaultValueOrOptions` holds the default value only when it is a string; otherwise it holds\n // the options object and `optionsOrUndefined` is not provided.\n const hasDefaultValue = typeof defaultValueOrOptions === \"string\";\n const options = (hasDefaultValue ? optionsOrUndefined : defaultValueOrOptions) ?? {};\n const defaultValue = hasDefaultValue\n ? defaultValueOrOptions\n : this.options.defaultMissingKey\n ? language(this.options.defaultMissingKey, { replace: { key } })\n : \"\";\n\n return language(\n key as never,\n {\n defaultValue,\n ...(options as TOpt),\n } as never,\n ) as TFunctionReturnOptionalDetails<\n TFunctionProcessReturnValue<$NoInfer<Ret>, DefaultValue>,\n TOpt\n >;\n }\n\n /**\n * Walks the root languages directory, collecting every language and namespace found in it.\n * @param directory The directory that should be walked.\n */\n public async walkRootDirectory(directory: PathLike) {\n const languages = new Set<string>();\n const namespaces = new Set<string>();\n\n const dir = await opendir(directory);\n for await (const entry of dir) {\n // If the entry is not a directory, skip:\n if (!entry.isDirectory()) continue;\n\n // Load the directory:\n languages.add(entry.name);\n\n for await (const namespace of this.walkLocaleDirectory(join(dir.path, entry.name), \"\")) {\n namespaces.add(namespace);\n }\n }\n\n return { namespaces: [...namespaces], languages: [...languages] };\n }\n\n /**\n * Reloads the languages and namespaces registered in i18next, used by the HMR watcher registered\n * in `@wolfstar/plugin-i18next/register`.\n */\n public reloadResources(): Promise<void> {\n // Serialize reloads: the watcher can fire several events for a single edit (`addDir` followed by\n // `add`, for instance), and overlapping `i18next.loadLanguages` calls would race each other.\n this.reloadTask = this.reloadTask.then(() => this.performReload());\n return this.reloadTask;\n }\n\n /**\n * Registers the languages and namespaces that were discovered after {@link init} ran, so locales\n * and namespaces added while the process is running become usable without a restart.\n * @param discovered The result of {@link walkRootDirectory}.\n */\n private async registerDiscoveredResources(discovered: {\n languages: string[];\n namespaces: string[];\n }) {\n const newNamespaces = discovered.namespaces.filter(\n (namespace) => !this.namespaces.has(namespace),\n );\n if (newNamespaces.length > 0) {\n await i18next.loadNamespaces(newNamespaces);\n for (const namespace of newNamespaces) this.namespaces.add(namespace);\n }\n\n const newLanguages = discovered.languages.filter((language) => !this.languages.has(language));\n if (newLanguages.length > 0) {\n await i18next.loadLanguages(newLanguages);\n for (const language of newLanguages)\n this.languages.set(language, i18next.getFixedT(language));\n }\n }\n\n /**\n * @internal\n */\n private async performReload() {\n const result = await Result.fromAsync(async () => {\n let languages = this.options.hmr?.languages;\n let namespaces = this.options.hmr?.namespaces;\n\n // Only walk the directory when at least one of the two was not pinned through the options,\n // matching what `init` discovers, and register anything that appeared since.\n if (!languages || !namespaces) {\n const languageDirectoryResult = await this.walkRootDirectory(this.languagesDirectory);\n await this.registerDiscoveredResources(languageDirectoryResult);\n languages ??= languageDirectoryResult.languages;\n namespaces ??= languageDirectoryResult.namespaces;\n }\n\n await i18next.reloadResources(languages, namespaces);\n console.info(\"[plugin-i18next] Reloaded language resources.\");\n });\n\n result.inspectErr((error: unknown) =>\n console.error(\"[plugin-i18next] Failed to reload language resources.\", error),\n );\n }\n\n /**\n * Walks a single locale directory, yielding every namespace found in it.\n *\n * @remarks\n * Skips any file that does not end with `.json`.\n * @param directory The directory that should be walked.\n * @param ns The current namespace.\n */\n private async *walkLocaleDirectory(directory: string, ns: string): AsyncGenerator<string> {\n const dir = await opendir(directory);\n for await (const entry of dir) {\n if (entry.isDirectory()) {\n yield* this.walkLocaleDirectory(join(dir.path, entry.name), `${ns}${entry.name}/`);\n } else if (entry.isFile() && entry.name.endsWith(\".json\")) {\n yield `${ns}${entry.name.slice(0, -5)}`;\n }\n }\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wolfstar/plugin-i18next",
3
- "version": "1.0.0-next-20260829115416",
3
+ "version": "1.0.0",
4
4
  "description": "Plugin for @wolfstar/http-framework adding i18next-powered internationalization for HTTP interactions",
5
5
  "keywords": [
6
6
  "discord",