@wolfstar/plugin-i18next 1.0.0-next-20260829113016
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/LICENSE +202 -0
- package/README.md +169 -0
- package/dist/esm/index.d.ts +520 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +438 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/register.d.ts +21 -0
- package/dist/esm/register.d.ts.map +1 -0
- package/dist/esm/register.js +35 -0
- package/dist/esm/register.js.map +1 -0
- package/package.json +76 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import i18next, { default as i18next$1 } from "i18next";
|
|
2
|
+
import { Collection } from "@discordjs/collection";
|
|
3
|
+
import { container, getRootData } from "@sapphire/pieces";
|
|
4
|
+
import { isFunction, lazy } from "@sapphire/utilities";
|
|
5
|
+
import { Locale } from "discord-api-types/v10";
|
|
6
|
+
import { Result } from "@sapphire/result";
|
|
7
|
+
import { Backend } from "@wolfstar/i18next-backend";
|
|
8
|
+
import { opendir } from "node:fs/promises";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
//#region src/lib/functions.ts
|
|
12
|
+
/**
|
|
13
|
+
* Brands a translation key with the type it resolves to.
|
|
14
|
+
* @param k The i18next key.
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* export const InvalidInput = T('path/to/file:invalidInput');
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
function T(k) {
|
|
21
|
+
return k;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Brands a translation key with both its interpolation arguments and the type it resolves to.
|
|
25
|
+
* @param k The i18next key.
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* export const AddResult = FT<{ left: number; right: number; result: number }>('path/to/file:addResult');
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
function FT(k) {
|
|
32
|
+
return k;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Every locale Discord supports.
|
|
36
|
+
*/
|
|
37
|
+
const supportedLanguages = new Set(Object.values(Locale));
|
|
38
|
+
/**
|
|
39
|
+
* Checks whether the given language is a locale Discord supports.
|
|
40
|
+
* @param language The language to check.
|
|
41
|
+
*/
|
|
42
|
+
function isSupportedDiscordLocale(language) {
|
|
43
|
+
return supportedLanguages.has(language);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolves the loaded language that best matches the user's locale, falling back to the guild's and
|
|
47
|
+
* then to `'en-US'`.
|
|
48
|
+
* @param interaction The interaction to read the locales from.
|
|
49
|
+
*/
|
|
50
|
+
function getSupportedUserLanguageName(interaction) {
|
|
51
|
+
const { languages } = container.i18n;
|
|
52
|
+
if (languages.has(interaction.locale)) return interaction.locale;
|
|
53
|
+
if (interaction.guild_locale && languages.has(interaction.guild_locale)) return interaction.guild_locale;
|
|
54
|
+
return "en-US";
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolves the `TFunction` for {@link getSupportedUserLanguageName}.
|
|
58
|
+
* @param interaction The interaction to read the locales from.
|
|
59
|
+
*/
|
|
60
|
+
function getSupportedUserLanguageT(interaction) {
|
|
61
|
+
return container.i18n.getT(getSupportedUserLanguageName(interaction));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolves the loaded language that best matches the guild's locale, falling back to the user's one
|
|
65
|
+
* when the interaction was not sent from a guild, and then to `'en-US'`.
|
|
66
|
+
* @param interaction The interaction to read the locales from.
|
|
67
|
+
*/
|
|
68
|
+
function getSupportedLanguageName(interaction) {
|
|
69
|
+
const { languages } = container.i18n;
|
|
70
|
+
if (interaction.guild_id) {
|
|
71
|
+
if (interaction.guild_locale && languages.has(interaction.guild_locale)) return interaction.guild_locale;
|
|
72
|
+
} else if (languages.has(interaction.locale)) return interaction.locale;
|
|
73
|
+
return "en-US";
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Resolves the `TFunction` for {@link getSupportedLanguageName}.
|
|
77
|
+
* @param interaction The interaction to read the locales from.
|
|
78
|
+
*/
|
|
79
|
+
function getSupportedLanguageT(interaction) {
|
|
80
|
+
return container.i18n.getT(getSupportedLanguageName(interaction));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Builds the {@link InternationalizationContext} for an interaction.
|
|
84
|
+
* @internal
|
|
85
|
+
*/
|
|
86
|
+
function getContext(interaction) {
|
|
87
|
+
return {
|
|
88
|
+
guildId: interaction.guild_id ?? null,
|
|
89
|
+
channelId: interaction.channel_id ?? null,
|
|
90
|
+
userId: interaction.user?.id ?? interaction.member?.user.id ?? null,
|
|
91
|
+
interactionGuildLocale: interaction.guild_locale,
|
|
92
|
+
interactionLocale: interaction.locale
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Retrieves the language name for a target, using {@link InternationalizationHandler.fetchLanguage}.
|
|
97
|
+
*
|
|
98
|
+
* If that hook is not defined or returns a nullish value, there will be a series of fallback
|
|
99
|
+
* attempts in the following descending order:
|
|
100
|
+
* 1. The result of {@link getSupportedLanguageName}, if it is a loaded language.
|
|
101
|
+
* 2. {@link InternationalizationOptions.defaultName}.
|
|
102
|
+
* 3. `'en-US'`.
|
|
103
|
+
* @param target The target to fetch the language from.
|
|
104
|
+
*/
|
|
105
|
+
async function fetchLanguage(target) {
|
|
106
|
+
return await container.i18n.fetchLanguage(getContext(target)) ?? getSupportedLanguageName(target) ?? container.i18n.options.defaultName ?? "en-US";
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Retrieves the language-assigned function from i18next designated to a target's preferred language.
|
|
110
|
+
* @param target The target to fetch the language from.
|
|
111
|
+
*/
|
|
112
|
+
async function fetchT(target) {
|
|
113
|
+
return container.i18n.getT(await fetchLanguage(target));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Resolves a key and its parameters using {@link fetchLanguage}, meaning a custom
|
|
117
|
+
* {@link InternationalizationHandler.fetchLanguage} hook (for example, a per-guild database lookup)
|
|
118
|
+
* is honoured.
|
|
119
|
+
*
|
|
120
|
+
* @remarks
|
|
121
|
+
* Use {@link resolveKey} when the language can be resolved from the interaction payload alone, it
|
|
122
|
+
* is synchronous and does not hit the hook.
|
|
123
|
+
* @param target The target to fetch the language key from.
|
|
124
|
+
*/
|
|
125
|
+
async function fetchKey(target, ...[key, defaultValueOrOptions, optionsOrUndefined]) {
|
|
126
|
+
const parsedOptions = typeof defaultValueOrOptions === "string" ? optionsOrUndefined : defaultValueOrOptions;
|
|
127
|
+
const language = typeof parsedOptions?.lng === "string" ? parsedOptions.lng : await fetchLanguage(target);
|
|
128
|
+
if (typeof defaultValueOrOptions === "string") return container.i18n.format(language, key, defaultValueOrOptions, optionsOrUndefined);
|
|
129
|
+
return container.i18n.format(language, key, void 0, defaultValueOrOptions);
|
|
130
|
+
}
|
|
131
|
+
function resolveUserKey(interaction, ...args) {
|
|
132
|
+
return getSupportedUserLanguageT(interaction)(...args);
|
|
133
|
+
}
|
|
134
|
+
function resolveKey(interaction, ...args) {
|
|
135
|
+
return getSupportedLanguageT(interaction)(...args);
|
|
136
|
+
}
|
|
137
|
+
const getLocales = lazy(() => {
|
|
138
|
+
const locales = new Collection();
|
|
139
|
+
for (const [locale, t] of container.i18n.languages) {
|
|
140
|
+
if (!isSupportedDiscordLocale(locale)) {
|
|
141
|
+
process.emitWarning("Unsupported Discord locale", {
|
|
142
|
+
code: "UNSUPPORTED_LOCALE",
|
|
143
|
+
detail: `'${locale}' is not assignable to type LocaleString`
|
|
144
|
+
});
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
locales.set(locale, t);
|
|
148
|
+
}
|
|
149
|
+
return locales;
|
|
150
|
+
});
|
|
151
|
+
const getDefaultT = lazy(() => {
|
|
152
|
+
const defaultLocale = container.i18n.options.defaultName ?? "en-US";
|
|
153
|
+
if (!isSupportedDiscordLocale(defaultLocale)) throw new TypeError(`Unsupported Discord locale found:\n'${defaultLocale}' is not within the list of ${[...supportedLanguages]}`);
|
|
154
|
+
const defaultT = getLocales().get(defaultLocale);
|
|
155
|
+
if (defaultT) return defaultT;
|
|
156
|
+
throw new TypeError(`Could not find ${defaultLocale}`);
|
|
157
|
+
});
|
|
158
|
+
/**
|
|
159
|
+
* Gets the value and the localizations from a language key.
|
|
160
|
+
* @param key The key to get the localizations from.
|
|
161
|
+
* @returns The retrieved data.
|
|
162
|
+
* @remarks This should be called **strictly** after loading the locales.
|
|
163
|
+
*/
|
|
164
|
+
function getLocalizedData(key) {
|
|
165
|
+
const locales = getLocales();
|
|
166
|
+
return {
|
|
167
|
+
value: getDefaultT()(key),
|
|
168
|
+
localizations: Object.fromEntries(locales.map((t, locale) => [locale, t(key)]))
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Applies the localized names on the builder, calling `setName` and `setNameLocalizations`.
|
|
173
|
+
* @param builder The builder to apply the localizations to.
|
|
174
|
+
* @param key The key to get the localizations from.
|
|
175
|
+
* @returns The updated builder.
|
|
176
|
+
*/
|
|
177
|
+
function applyNameLocalizedBuilder(builder, key) {
|
|
178
|
+
const result = getLocalizedData(key);
|
|
179
|
+
return builder.setName(result.value).setNameLocalizations(result.localizations);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Applies the localized descriptions on the builder, calling `setDescription` and
|
|
183
|
+
* `setDescriptionLocalizations`.
|
|
184
|
+
* @param builder The builder to apply the localizations to.
|
|
185
|
+
* @param key The key to get the localizations from.
|
|
186
|
+
* @returns The updated builder.
|
|
187
|
+
*/
|
|
188
|
+
function applyDescriptionLocalizedBuilder(builder, key) {
|
|
189
|
+
const result = getLocalizedData(key);
|
|
190
|
+
return builder.setDescription(result.value).setDescriptionLocalizations(result.localizations);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Applies the localized names and descriptions on the builder, calling
|
|
194
|
+
* {@link applyNameLocalizedBuilder} and {@link applyDescriptionLocalizedBuilder}.
|
|
195
|
+
*
|
|
196
|
+
* @param builder The builder to apply the localizations to.
|
|
197
|
+
* @param params The root key, or the key for the name and the key for the description.
|
|
198
|
+
* @returns The updated builder. You can chain subsequent builder methods on this.
|
|
199
|
+
*
|
|
200
|
+
* @remarks
|
|
201
|
+
* If only 2 parameters were passed, `name` will be defined as `${root}Name` and `description` as
|
|
202
|
+
* `${root}Description`, being `root` the second parameter in the function, after `builder`.
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```typescript
|
|
206
|
+
* // Both keys given explicitly:
|
|
207
|
+
* applyLocalizedBuilder(builder, 'commands/names:userinfo', 'commands/descriptions:userinfo');
|
|
208
|
+
* ```
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```typescript
|
|
212
|
+
* // Root key only, resolves `commands/userinfo:nameName` and `commands/userinfo:nameDescription`:
|
|
213
|
+
* applyLocalizedBuilder(builder, 'commands/userinfo:name');
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
function applyLocalizedBuilder(builder, ...params) {
|
|
217
|
+
const [localeName, localeDescription] = params.length === 1 ? [`${params[0]}Name`, `${params[0]}Description`] : params;
|
|
218
|
+
applyNameLocalizedBuilder(builder, localeName);
|
|
219
|
+
applyDescriptionLocalizedBuilder(builder, localeDescription);
|
|
220
|
+
return builder;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Constructs an object that can be passed into `setChoices` for a String or Number option with
|
|
224
|
+
* localized names.
|
|
225
|
+
*
|
|
226
|
+
* @param key The i18next key for the name of the choice.
|
|
227
|
+
* @param options The remaining choice options. This should _at least_ include the `value` key.
|
|
228
|
+
* @returns An object with anything provided through `options`, with `name` and `name_localizations`
|
|
229
|
+
* added.
|
|
230
|
+
*/
|
|
231
|
+
function createLocalizedChoice(key, options) {
|
|
232
|
+
const result = getLocalizedData(key);
|
|
233
|
+
return {
|
|
234
|
+
...options,
|
|
235
|
+
name: result.value,
|
|
236
|
+
name_localizations: result.localizations
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Constructs a select menu option with a localized `name`, spreading any extra value on top.
|
|
241
|
+
* @param key The i18next key for the name of the select option.
|
|
242
|
+
* @param value The additional select option properties.
|
|
243
|
+
*/
|
|
244
|
+
function createSelectMenuChoiceName(key, value) {
|
|
245
|
+
const result = getLocalizedData(key);
|
|
246
|
+
return {
|
|
247
|
+
...value,
|
|
248
|
+
name: result.value,
|
|
249
|
+
name_localizations: result.localizations
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/lib/InternationalizationHandler.ts
|
|
255
|
+
/**
|
|
256
|
+
* A generalized class for handling `i18next` JSON files and their discovery.
|
|
257
|
+
*/
|
|
258
|
+
var InternationalizationHandler = class {
|
|
259
|
+
/**
|
|
260
|
+
* Describes whether {@link InternationalizationHandler.init} has been run and languages are
|
|
261
|
+
* loaded in {@link InternationalizationHandler.languages}.
|
|
262
|
+
*/
|
|
263
|
+
languagesLoaded = false;
|
|
264
|
+
/**
|
|
265
|
+
* A `Set` of initially loaded namespaces.
|
|
266
|
+
*/
|
|
267
|
+
namespaces = /* @__PURE__ */ new Set();
|
|
268
|
+
/**
|
|
269
|
+
* A `Map` of `i18next` language functions keyed by their language code.
|
|
270
|
+
*/
|
|
271
|
+
languages = /* @__PURE__ */ new Map();
|
|
272
|
+
/**
|
|
273
|
+
* The options {@link InternationalizationHandler} was initialized with.
|
|
274
|
+
*/
|
|
275
|
+
options;
|
|
276
|
+
/**
|
|
277
|
+
* The directory passed to `@wolfstar/i18next-backend`. Also used in
|
|
278
|
+
* {@link InternationalizationHandler.walkRootDirectory}.
|
|
279
|
+
*/
|
|
280
|
+
languagesDirectory;
|
|
281
|
+
/**
|
|
282
|
+
* The backend options for `@wolfstar/i18next-backend` used by `i18next`.
|
|
283
|
+
*/
|
|
284
|
+
backendOptions;
|
|
285
|
+
/**
|
|
286
|
+
* @param options The options that `i18next`, `@wolfstar/i18next-backend`, and
|
|
287
|
+
* {@link InternationalizationHandler} should use.
|
|
288
|
+
*/
|
|
289
|
+
constructor(options) {
|
|
290
|
+
this.options = options ?? { i18next: { ignoreJSONStructure: false } };
|
|
291
|
+
this.languagesDirectory = this.options.defaultLanguageDirectory ?? join(getRootData().root, "languages");
|
|
292
|
+
const languagePaths = /* @__PURE__ */ new Set([join(this.languagesDirectory, "{{lng}}", "{{ns}}.json"), ...options?.backend?.paths ?? []]);
|
|
293
|
+
this.backendOptions = {
|
|
294
|
+
paths: [...languagePaths],
|
|
295
|
+
...this.options.backend
|
|
296
|
+
};
|
|
297
|
+
if (isFunction(this.options.fetchLanguage)) this.fetchLanguage = this.options.fetchLanguage;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* The method to be overridden by the developer.
|
|
301
|
+
*
|
|
302
|
+
* @remarks
|
|
303
|
+
* In the event that `fetchLanguage` is not defined or returns null / undefined, the interaction's
|
|
304
|
+
* locales are used instead.
|
|
305
|
+
* @returns A string for the desired language or null for no match.
|
|
306
|
+
* @example
|
|
307
|
+
* ```typescript
|
|
308
|
+
* // Always use the same language (no per-guild configuration):
|
|
309
|
+
* container.i18n.fetchLanguage = () => 'en-US';
|
|
310
|
+
* ```
|
|
311
|
+
* @example
|
|
312
|
+
* ```typescript
|
|
313
|
+
* // Retrieving the language from an ORM:
|
|
314
|
+
* container.i18n.fetchLanguage = async (context) => {
|
|
315
|
+
* if (!context.guildId) return null;
|
|
316
|
+
* const guild = await driver.getRepository(GuildEntity).findOne({ id: context.guildId });
|
|
317
|
+
* return guild?.language ?? 'en-US';
|
|
318
|
+
* };
|
|
319
|
+
* ```
|
|
320
|
+
*/
|
|
321
|
+
fetchLanguage = () => null;
|
|
322
|
+
/**
|
|
323
|
+
* Initializes the handler by loading in the namespaces, passing the data to i18next, and filling
|
|
324
|
+
* in {@link InternationalizationHandler.languages}.
|
|
325
|
+
*/
|
|
326
|
+
async init() {
|
|
327
|
+
const { namespaces, languages } = await this.walkRootDirectory(this.languagesDirectory);
|
|
328
|
+
const userOptions = isFunction(this.options.i18next) ? this.options.i18next(namespaces, languages) : this.options.i18next;
|
|
329
|
+
const ignoreJSONStructure = userOptions?.ignoreJSONStructure ?? false;
|
|
330
|
+
const skipOnVariables = userOptions?.interpolation?.skipOnVariables ?? false;
|
|
331
|
+
i18next$1.use(Backend);
|
|
332
|
+
await i18next$1.init({
|
|
333
|
+
backend: this.backendOptions,
|
|
334
|
+
fallbackLng: this.options.defaultName ?? "en-US",
|
|
335
|
+
initImmediate: false,
|
|
336
|
+
interpolation: {
|
|
337
|
+
escapeValue: false,
|
|
338
|
+
...userOptions?.interpolation,
|
|
339
|
+
skipOnVariables
|
|
340
|
+
},
|
|
341
|
+
load: "all",
|
|
342
|
+
defaultNS: this.options.defaultNS ?? "default",
|
|
343
|
+
ns: namespaces,
|
|
344
|
+
preload: languages,
|
|
345
|
+
...userOptions,
|
|
346
|
+
ignoreJSONStructure
|
|
347
|
+
});
|
|
348
|
+
this.namespaces = new Set(namespaces);
|
|
349
|
+
for (const item of languages) this.languages.set(item, i18next$1.getFixedT(item));
|
|
350
|
+
this.languagesLoaded = true;
|
|
351
|
+
const formatter = i18next$1.services.formatter;
|
|
352
|
+
for (const { name, format, cached } of this.options.formatters ?? []) if (cached) formatter.addCached(name, format);
|
|
353
|
+
else formatter.add(name, format);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Retrieve a raw `TFunction` from the passed locale.
|
|
357
|
+
* @param locale The language to be used.
|
|
358
|
+
*/
|
|
359
|
+
getT(locale) {
|
|
360
|
+
if (!this.languagesLoaded) throw new Error("Cannot call this method until InternationalizationHandler#init has been called");
|
|
361
|
+
const t = this.languages.get(locale);
|
|
362
|
+
if (t) return t;
|
|
363
|
+
throw new ReferenceError(`Invalid language (${locale})`);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Localizes a content given one or more keys and i18next options.
|
|
367
|
+
* @param locale The language to be used.
|
|
368
|
+
*
|
|
369
|
+
* @remarks
|
|
370
|
+
* This function also has additional parameters for `key`, `defaultValue`, and `options`, however
|
|
371
|
+
* TSDoc does not let us document those while matching the implementation signature. See the
|
|
372
|
+
* overloads for this method for the documentation on those parameters.
|
|
373
|
+
*
|
|
374
|
+
* @see {@link https://www.i18next.com/overview/api#t}
|
|
375
|
+
* @returns The localized content.
|
|
376
|
+
*/
|
|
377
|
+
format(locale, ...[key, defaultValueOrOptions, optionsOrUndefined]) {
|
|
378
|
+
const language = this.getT(locale);
|
|
379
|
+
const hasDefaultValue = typeof defaultValueOrOptions === "string";
|
|
380
|
+
const options = (hasDefaultValue ? optionsOrUndefined : defaultValueOrOptions) ?? {};
|
|
381
|
+
return language(key, {
|
|
382
|
+
defaultValue: hasDefaultValue ? defaultValueOrOptions : this.options.defaultMissingKey ? language(this.options.defaultMissingKey, { replace: { key } }) : "",
|
|
383
|
+
...options
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Walks the root languages directory, collecting every language and namespace found in it.
|
|
388
|
+
* @param directory The directory that should be walked.
|
|
389
|
+
*/
|
|
390
|
+
async walkRootDirectory(directory) {
|
|
391
|
+
const languages = /* @__PURE__ */ new Set();
|
|
392
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
393
|
+
const dir = await opendir(directory);
|
|
394
|
+
for await (const entry of dir) {
|
|
395
|
+
if (!entry.isDirectory()) continue;
|
|
396
|
+
languages.add(entry.name);
|
|
397
|
+
for await (const namespace of this.walkLocaleDirectory(join(dir.path, entry.name), "")) namespaces.add(namespace);
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
namespaces: [...namespaces],
|
|
401
|
+
languages: [...languages]
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Reloads the languages and namespaces registered in i18next, used by the HMR watcher registered
|
|
406
|
+
* in `@wolfstar/plugin-i18next/register`.
|
|
407
|
+
*/
|
|
408
|
+
async reloadResources() {
|
|
409
|
+
(await Result.fromAsync(async () => {
|
|
410
|
+
let languages = this.options.hmr?.languages;
|
|
411
|
+
let namespaces = this.options.hmr?.namespaces;
|
|
412
|
+
if (!languages || !namespaces) {
|
|
413
|
+
const languageDirectoryResult = await this.walkRootDirectory(this.languagesDirectory);
|
|
414
|
+
languages ??= languageDirectoryResult.languages;
|
|
415
|
+
namespaces ??= languageDirectoryResult.namespaces;
|
|
416
|
+
}
|
|
417
|
+
await i18next$1.reloadResources(languages, namespaces);
|
|
418
|
+
console.info("[plugin-i18next] Reloaded language resources.");
|
|
419
|
+
})).inspectErr((error) => console.error("[plugin-i18next] Failed to reload language resources.", error));
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Walks a single locale directory, yielding every namespace found in it.
|
|
423
|
+
*
|
|
424
|
+
* @remarks
|
|
425
|
+
* Skips any file that does not end with `.json`.
|
|
426
|
+
* @param directory The directory that should be walked.
|
|
427
|
+
* @param ns The current namespace.
|
|
428
|
+
*/
|
|
429
|
+
async *walkLocaleDirectory(directory, ns) {
|
|
430
|
+
const dir = await opendir(directory);
|
|
431
|
+
for await (const entry of dir) if (entry.isDirectory()) yield* this.walkLocaleDirectory(join(dir.path, entry.name), `${ns}${entry.name}/`);
|
|
432
|
+
else if (entry.isFile() && entry.name.endsWith(".json")) yield `${ns}${entry.name.slice(0, -5)}`;
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
//#endregion
|
|
437
|
+
export { FT, InternationalizationHandler, T, applyDescriptionLocalizedBuilder, applyLocalizedBuilder, applyNameLocalizedBuilder, createLocalizedChoice, createSelectMenuChoiceName, fetchKey, fetchLanguage, fetchT, getLocalizedData, getSupportedLanguageName, getSupportedLanguageT, getSupportedUserLanguageName, getSupportedUserLanguageT, i18next, isSupportedDiscordLocale, resolveKey, resolveUserKey, supportedLanguages };
|
|
438
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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 { lazy, 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 loaded language that best matches the user's locale, falling back to the guild's and\n * then 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 \"en-US\";\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, and then 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 \"en-US\";\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, there will be a series of fallback\n * attempts in the following descending order:\n * 1. The result of {@link getSupportedLanguageName}, if it is a loaded language.\n * 2. {@link InternationalizationOptions.defaultName}.\n * 3. `'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 (\n language ?? getSupportedLanguageName(target) ?? container.i18n.options.defaultName ?? \"en-US\"\n );\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\nconst getLocales = lazy(() => {\n const locales = new Collection<LocaleString, TFunction>();\n\n for (const [locale, t] of container.i18n.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 return locales;\n});\n\nconst getDefaultT = lazy(() => {\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 * @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 async reloadResources() {\n const result = await Result.fromAsync(async () => {\n let languages = this.options.hmr?.languages;\n let namespaces = this.options.hmr?.namespaces;\n if (!languages || !namespaces) {\n const languageDirectoryResult = await this.walkRootDirectory(this.languagesDirectory);\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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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import "./index.js";
|
|
2
|
+
import { Client, ClientOptions, Plugin, postListen, preGenericsInitialization, preLoad } from "@wolfstar/http-framework";
|
|
3
|
+
//#region src/register.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Registers the i18next-powered {@link InternationalizationHandler} on `container.i18n`, loading the
|
|
6
|
+
* languages before the stores are loaded so command builders can be localized at registration time.
|
|
7
|
+
*
|
|
8
|
+
* Activate by importing the side-effecting entrypoint before creating the client:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import '@wolfstar/plugin-i18next/register';
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
declare class I18nextPlugin extends Plugin {
|
|
15
|
+
static [preGenericsInitialization](this: Client, options: ClientOptions): void;
|
|
16
|
+
static [preLoad](this: Client): Promise<void>;
|
|
17
|
+
static [postListen](this: Client, options: ClientOptions): void;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { I18nextPlugin };
|
|
21
|
+
//# sourceMappingURL=register.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.d.ts","names":[],"sources":["../../src/register.ts"],"mappings":";;;;;;;;;;;;;cAuBa,sBAAsB;UAClB,2BAA2B,MAAM,QAAQ,SAAS;UAI5C,SAAS,MAAM,SAAS;UAI9B,YAAY,MAAM,QAAQ,SAAS"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { InternationalizationHandler } from "./index.js";
|
|
2
|
+
import { Client, Plugin, container, postListen, preGenericsInitialization, preLoad } from "@wolfstar/http-framework";
|
|
3
|
+
import { watch } from "chokidar";
|
|
4
|
+
|
|
5
|
+
//#region src/register.ts
|
|
6
|
+
/**
|
|
7
|
+
* Registers the i18next-powered {@link InternationalizationHandler} on `container.i18n`, loading the
|
|
8
|
+
* languages before the stores are loaded so command builders can be localized at registration time.
|
|
9
|
+
*
|
|
10
|
+
* Activate by importing the side-effecting entrypoint before creating the client:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import '@wolfstar/plugin-i18next/register';
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
var I18nextPlugin = class extends Plugin {
|
|
17
|
+
static [preGenericsInitialization](options) {
|
|
18
|
+
container.i18n = new InternationalizationHandler(options.i18n);
|
|
19
|
+
}
|
|
20
|
+
static async [preLoad]() {
|
|
21
|
+
await container.i18n.init();
|
|
22
|
+
}
|
|
23
|
+
static [postListen](options) {
|
|
24
|
+
if (!options.i18n?.hmr?.enabled) return;
|
|
25
|
+
console.info("[plugin-i18next] HMR enabled. Watching for language changes.");
|
|
26
|
+
watch(container.i18n.languagesDirectory, options.i18n.hmr.options ?? {}).on("change", () => void container.i18n.reloadResources()).on("unlink", () => void container.i18n.reloadResources());
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
Client.plugins.registerPreGenericsInitializationHook(I18nextPlugin[preGenericsInitialization], "WolfStar-I18next-PreGenericsInitialization");
|
|
30
|
+
Client.plugins.registerPreLoadHook(I18nextPlugin[preLoad], "WolfStar-I18next-PreLoad");
|
|
31
|
+
Client.plugins.registerPostListenHook(I18nextPlugin[postListen], "WolfStar-I18next-PostListen");
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
export { I18nextPlugin };
|
|
35
|
+
//# sourceMappingURL=register.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.js","names":[],"sources":["../../src/register.ts"],"sourcesContent":["import {\n Client,\n container,\n Plugin,\n postListen,\n preGenericsInitialization,\n preLoad,\n type ClientOptions,\n} from \"@wolfstar/http-framework\";\nimport { watch } from \"chokidar\";\nimport \"./index\";\nimport { InternationalizationHandler } from \"./lib/InternationalizationHandler\";\n\n/**\n * Registers the i18next-powered {@link InternationalizationHandler} on `container.i18n`, loading the\n * languages before the stores are loaded so command builders can be localized at registration time.\n *\n * Activate by importing the side-effecting entrypoint before creating the client:\n *\n * ```ts\n * import '@wolfstar/plugin-i18next/register';\n * ```\n */\nexport class I18nextPlugin extends Plugin {\n public static [preGenericsInitialization](this: Client, options: ClientOptions): void {\n container.i18n = new InternationalizationHandler(options.i18n);\n }\n\n public static async [preLoad](this: Client): Promise<void> {\n await container.i18n.init();\n }\n\n public static [postListen](this: Client, options: ClientOptions): void {\n if (!options.i18n?.hmr?.enabled) return;\n\n console.info(\"[plugin-i18next] HMR enabled. Watching for language changes.\");\n\n watch(container.i18n.languagesDirectory, options.i18n.hmr.options ?? {})\n .on(\"change\", () => void container.i18n.reloadResources())\n .on(\"unlink\", () => void container.i18n.reloadResources());\n }\n}\n\nClient.plugins.registerPreGenericsInitializationHook(\n I18nextPlugin[preGenericsInitialization],\n \"WolfStar-I18next-PreGenericsInitialization\",\n);\nClient.plugins.registerPreLoadHook(I18nextPlugin[preLoad], \"WolfStar-I18next-PreLoad\");\nClient.plugins.registerPostListenHook(I18nextPlugin[postListen], \"WolfStar-I18next-PostListen\");\n"],"mappings":";;;;;;;;;;;;;;;AAuBA,IAAa,gBAAb,cAAmC,OAAO;CACxC,QAAe,2BAAyC,SAA8B;EACpF,UAAU,OAAO,IAAI,4BAA4B,QAAQ,IAAI;CAC/D;CAEA,cAAqB,WAAsC;EACzD,MAAM,UAAU,KAAK,KAAK;CAC5B;CAEA,QAAe,YAA0B,SAA8B;EACrE,IAAI,CAAC,QAAQ,MAAM,KAAK,SAAS;EAEjC,QAAQ,KAAK,8DAA8D;EAE3E,MAAM,UAAU,KAAK,oBAAoB,QAAQ,KAAK,IAAI,WAAW,CAAC,CAAC,CAAC,CACrE,GAAG,gBAAgB,KAAK,UAAU,KAAK,gBAAgB,CAAC,CAAC,CACzD,GAAG,gBAAgB,KAAK,UAAU,KAAK,gBAAgB,CAAC;CAC7D;AACF;AAEA,OAAO,QAAQ,sCACb,cAAc,4BACd,4CACF;AACA,OAAO,QAAQ,oBAAoB,cAAc,UAAU,0BAA0B;AACrF,OAAO,QAAQ,uBAAuB,cAAc,aAAa,6BAA6B"}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wolfstar/plugin-i18next",
|
|
3
|
+
"version": "1.0.0-next-20260829113016",
|
|
4
|
+
"description": "Plugin for @wolfstar/http-framework adding i18next-powered internationalization for HTTP interactions",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"discord",
|
|
7
|
+
"http-framework",
|
|
8
|
+
"i18n",
|
|
9
|
+
"i18next",
|
|
10
|
+
"plugin",
|
|
11
|
+
"wolfstar"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/wolfstar-project/plugins/tree/main/packages/plugin-i18next",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/wolfstar-project/plugins/issues"
|
|
16
|
+
},
|
|
17
|
+
"license": "Apache-2.0",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/wolfstar-project/plugins.git",
|
|
21
|
+
"directory": "packages/plugin-i18next"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"sideEffects": [
|
|
28
|
+
"./dist/esm/register.js"
|
|
29
|
+
],
|
|
30
|
+
"main": "./dist/esm/index.js",
|
|
31
|
+
"module": "./dist/esm/index.js",
|
|
32
|
+
"types": "./dist/esm/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"import": {
|
|
36
|
+
"types": "./dist/esm/index.d.ts",
|
|
37
|
+
"default": "./dist/esm/index.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"./register": {
|
|
41
|
+
"import": {
|
|
42
|
+
"types": "./dist/esm/register.d.ts",
|
|
43
|
+
"default": "./dist/esm/register.js"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public",
|
|
49
|
+
"provenance": true
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@discordjs/collection": "^2.1.1",
|
|
53
|
+
"@sapphire/pieces": "^4.4.1",
|
|
54
|
+
"@sapphire/result": "^2.8.0",
|
|
55
|
+
"@sapphire/utilities": "^3.18.2",
|
|
56
|
+
"@wolfstar/i18next-backend": "^2.0.10",
|
|
57
|
+
"chokidar": "^4.0.3",
|
|
58
|
+
"discord-api-types": "^0.38.33",
|
|
59
|
+
"i18next": "^25.8.18"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@wolfstar/http-framework": "^3.1.2"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"@wolfstar/http-framework": "^3.1.0"
|
|
66
|
+
},
|
|
67
|
+
"engines": {
|
|
68
|
+
"node": ">=20.0.0"
|
|
69
|
+
},
|
|
70
|
+
"scripts": {
|
|
71
|
+
"build": "tsdown --config-loader unrun",
|
|
72
|
+
"typecheck": "tsc --noEmit",
|
|
73
|
+
"lint": "oxlint src --fix",
|
|
74
|
+
"test": "vitest run"
|
|
75
|
+
}
|
|
76
|
+
}
|