Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .npm-deprecaterc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package:
- '@wolfstar/http-framework'
- '@wolfstar/http-framework-i18n'
- '@wolfstar/i18next-backend'
- '@wolfstar/i18next-type-generator'
- '@wolfstar/influx-utilities'
- '@wolfstar/logger'
- '@wolfstar/reddit-helpers'
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
[![npm](https://img.shields.io/npm/v/@wolfstar/http-framework?color=crimson&logo=npm&style=flat-square&label=@wolfstar/http-framework)](https://www.npmjs.com/package/@wolfstar/http-framework)
[![npm](https://img.shields.io/npm/v/@wolfstar/http-framework-i18n?color=crimson&logo=npm&style=flat-square&label=@wolfstar/http-framework-i18n)](https://www.npmjs.com/package/@wolfstar/http-framework-i18n)
[![npm](https://img.shields.io/npm/v/@wolfstar/i18next-backend?color=crimson&logo=npm&style=flat-square&label=@wolfstar/i18next-backend)](https://www.npmjs.com/package/@wolfstar/i18next-backend)
[![npm](https://img.shields.io/npm/v/@wolfstar/i18next-type-generator?color=crimson&logo=npm&style=flat-square&label=@wolfstar/i18next-type-generator)](https://www.npmjs.com/package/@wolfstar/i18next-type-generator)
[![npm](https://img.shields.io/npm/v/@wolfstar/influx-utilities?color=crimson&logo=npm&style=flat-square&label=@wolfstar/influx-utilities)](https://www.npmjs.com/package/@wolfstar/influx-utilities)
[![npm](https://img.shields.io/npm/v/@wolfstar/logger?color=crimson&logo=npm&style=flat-square&label=@wolfstar/logger)](https://www.npmjs.com/package/@wolfstar/logger)
[![npm](https://img.shields.io/npm/v/@wolfstar/reddit-helpers?color=crimson&logo=npm&style=flat-square&label=@wolfstar/reddit-helpers)](https://www.npmjs.com/package/@wolfstar/reddit-helpers)
Expand Down
23 changes: 15 additions & 8 deletions packages/http-framework-i18n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,26 @@ addFormatters(
await init();
```

> **Note**: If you want to customize the options, please check [i18next's TypeScript guide](https://www.i18next.com/overview/typescript) to improve the experience.

### Definition

```typescript
import { T, FT } from '@wolfstar/http-framework-i18n';
Generate type augmentations with [`@wolfstar/i18next-type-generator`](https://www.npmjs.com/package/@wolfstar/i18next-type-generator):

export const InvalidInput = T('path/to/file:invalidInput');
export const AddResult = FT<{ left: number; right: number; result: number }>('path/to/file:addResult');
```bash
i18next-type-generator ./src/locales/en-US/ ./src/@types/i18next.d.ts
```

### Consumption

```typescript
import { getSupportedLanguageName, getSupportedUserLanguageName, getT, resolveKey, resolveUserKey } from '@wolfstar/http-framework-i18n';
import {
getSupportedLanguageName,
getSupportedUserLanguageName,
getT,
getSupportedLanguageT,
getSupportedUserLanguageT
} from '@wolfstar/http-framework-i18n';

// Get the name of the supported guild language, falling back to the user's on DMs:
const guildLanguage = getSupportedLanguageName(interaction);
Expand All @@ -55,11 +62,11 @@ const guildLanguage = getSupportedLanguageName(interaction);
const userLanguage = getSupportedUserLanguageName(interaction);

// Get the function to get a translated key:
const t = getT(guildLanguage);
const t = getT(guildLanguage, 'commands/shared');

// Resolving a given key, this calls `getT` and `getSupportedLanguageName` under the hood:
const content = resolveKey(interaction, InvalidInput);
const content = getSupportedLanguageT(interaction, 'commands/shared')('invalidInput');

// Resolving a given key, this calls `getT` and `getSupportedUserLanguageName` under the hood:
const content = resolveUserKey(interaction, AddResult, { left: 5, right: 10, result: 15 });
const content = getSupportedUserLanguageT(interaction, 'commands/shared')('addResult', { left: 5, right: 10, result: 15 });
```
2 changes: 1 addition & 1 deletion packages/http-framework-i18n/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"@sapphire/utilities": "^3.18.2",
"@wolfstar/i18next-backend": "workspace:^",
"discord-api-types": "^0.38.8",
"i18next": "^22.5.1",
"i18next": "^25.10.10",
"tslib": "^2.8.1"
},
"devDependencies": {
Expand Down
17 changes: 0 additions & 17 deletions packages/http-framework-i18n/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,3 @@
export { default as i18next, type InitOptions, type TFunction, type TOptions, type TOptionsBase } from 'i18next';
export * from './lib/functions.js';
export * from './lib/registry.js';
export type * from './lib/types.js';
export * from './lib/utils.js';

import type { NonNullObject } from '@sapphire/utilities';
import type { TypedFT, TypedT } from './lib/types.js';

declare module 'i18next' {
export interface TFunction {
lng: string;
ns?: string;

<TReturn>(key: TypedT<TReturn>, options?: TOptionsBase | string): TReturn;
<TReturn>(key: TypedT<TReturn>, defaultValue: TReturn, options?: TOptionsBase | string): TReturn;
<TArgs extends NonNullObject, TReturn>(key: TypedFT<TArgs, TReturn>, options?: TOptions<TArgs>): TReturn;
<TArgs extends NonNullObject, TReturn>(key: TypedFT<TArgs, TReturn>, defaultValue: TReturn, options?: TOptions<TArgs>): TReturn;
}
}
10 changes: 0 additions & 10 deletions packages/http-framework-i18n/src/lib/functions.ts

This file was deleted.

8 changes: 3 additions & 5 deletions packages/http-framework-i18n/src/lib/registry.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Collection } from '@discordjs/collection';
import { Backend } from '@wolfstar/i18next-backend';
import { Locale, type LocaleString } from 'discord-api-types/v10';
import i18next, { getFixedT, type InitOptions, type TFunction } from 'i18next';
import i18next, { getFixedT, type DefaultNamespace, type InitOptions, type Namespace, type TFunction } from 'i18next';
import type { PathLike } from 'node:fs';
import { opendir } from 'node:fs/promises';
import { join } from 'node:path';
Expand Down Expand Up @@ -85,8 +84,7 @@ async function loadLocale(directory: string, ns: string) {
}
}

const fixedCache = new Collection<LocaleString, TFunction>();
export function getT(locale: LocaleString): TFunction<'translation', undefined, 'translation'> {
export function getT<const Ns extends Namespace = DefaultNamespace>(locale: LocaleString, namespace?: Ns): TFunction<Ns> {
if (!loadedLocales.has(locale)) throw new ReferenceError(`Invalid language (${locale})`);
return fixedCache.ensure(locale, () => getFixedT(locale));
return getFixedT<Ns>(locale, namespace);
}
21 changes: 0 additions & 21 deletions packages/http-framework-i18n/src/lib/types.ts

This file was deleted.

64 changes: 13 additions & 51 deletions packages/http-framework-i18n/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@ import { Collection } from '@discordjs/collection';
import type { NonNullObject } from '@sapphire/utilities';
import { lazy } from '@sapphire/utilities';
import type { APIInteraction, APIPingInteraction, LocaleString, LocalizationMap } from 'discord-api-types/v10';
import type { TFunction, TOptions, TOptionsBase } from 'i18next';
import type { DefaultNamespace, Namespace, TFunction, TypeOptions } from 'i18next';
import { getT, loadedLocales } from './registry.js';
import type { LocalePrefixKey, TypedFT, TypedT } from './types.js';

export type Interaction = Pick<Exclude<APIInteraction, APIPingInteraction>, 'locale' | 'guild_locale' | 'guild_id'>;
export type LocaleSeparator = TypeOptions['nsSeparator'];
export type LocalePrefixKey = `${string}${LocaleSeparator}${string}`;

export function getSupportedUserLanguageName(interaction: Interaction): LocaleString {
if (loadedLocales.has(interaction.locale)) return interaction.locale;
if (interaction.guild_locale && loadedLocales.has(interaction.guild_locale)) return interaction.guild_locale;
return 'en-US';
}

export function getSupportedUserLanguageT(interaction: Interaction): TFunction {
return getT(getSupportedUserLanguageName(interaction));
export function getSupportedUserLanguageT<const Ns extends Namespace = DefaultNamespace>(interaction: Interaction, namespace?: Ns): TFunction<Ns> {
return getT(getSupportedUserLanguageName(interaction), namespace);
}

export function getSupportedLanguageName(interaction: Interaction): LocaleString {
Expand All @@ -27,47 +28,8 @@ export function getSupportedLanguageName(interaction: Interaction): LocaleString
return 'en-US';
}

export function getSupportedLanguageT(interaction: Interaction): TFunction {
return getT(getSupportedLanguageName(interaction));
}

export function resolveUserKey<TReturn>(interaction: Interaction, key: TypedT<TReturn>, options?: TOptionsBase | string): TReturn;
export function resolveUserKey<TReturn>(
interaction: Interaction,
key: TypedT<TReturn>,
defaultValue: TReturn,
options?: TOptionsBase | string
): TReturn;
export function resolveUserKey<TArgs extends NonNullObject, TReturn>(
interaction: Interaction,
key: TypedFT<TArgs, TReturn>,
options?: TOptions<TArgs>
): TReturn;
export function resolveUserKey<TArgs extends NonNullObject, TReturn>(
interaction: Interaction,
key: TypedFT<TArgs, TReturn>,
defaultValue: TReturn,
options?: TOptions<TArgs>
): TReturn;
export function resolveUserKey(interaction: Interaction, ...args: [any, any, any?]) {
return getSupportedUserLanguageT(interaction)(...args);
}

export function resolveKey<TReturn>(interaction: Interaction, key: TypedT<TReturn>, options?: TOptionsBase | string): TReturn;
export function resolveKey<TReturn>(interaction: Interaction, key: TypedT<TReturn>, defaultValue: TReturn, options?: TOptionsBase | string): TReturn;
export function resolveKey<TArgs extends NonNullObject, TReturn>(
interaction: Interaction,
key: TypedFT<TArgs, TReturn>,
options?: TOptions<TArgs>
): TReturn;
export function resolveKey<TArgs extends NonNullObject, TReturn>(
interaction: Interaction,
key: TypedFT<TArgs, TReturn>,
defaultValue: TReturn,
options?: TOptions<TArgs>
): TReturn;
export function resolveKey(interaction: Interaction, ...args: [any, any, any?]) {
return getSupportedLanguageT(interaction)(...args);
export function getSupportedLanguageT<const Ns extends Namespace = DefaultNamespace>(interaction: Interaction, namespace?: Ns): TFunction<Ns> {
return getT(getSupportedLanguageName(interaction), namespace);
}

const getLocales = lazy(() => new Collection([...loadedLocales].map((locale) => [locale, getT(locale)])));
Expand All @@ -83,7 +45,7 @@ const getDefaultT = lazy(() => {
* @returns The retrieved data.
* @remarks This should be called **strictly** after loading the locales.
*/
export function getLocalizedData(key: TypedT): LocalizedData {
export function getLocalizedData(key: LocalePrefixKey): LocalizedData {
const locales = getLocales();
const defaultT = getDefaultT();

Expand All @@ -99,7 +61,7 @@ export function getLocalizedData(key: TypedT): LocalizedData {
* @param key The key to get the localizations from.
* @returns The updated builder.
*/
export function applyNameLocalizedBuilder<T extends BuilderWithName>(builder: T, key: TypedT) {
export function applyNameLocalizedBuilder<T extends BuilderWithName>(builder: T, key: LocalePrefixKey) {
const result = getLocalizedData(key);
return builder.setName(result.value).setNameLocalizations(result.localizations);
}
Expand All @@ -110,7 +72,7 @@ export function applyNameLocalizedBuilder<T extends BuilderWithName>(builder: T,
* @param key The key to get the localizations from.
* @returns The updated builder.
*/
export function applyDescriptionLocalizedBuilder<T extends BuilderWithDescription>(builder: T, key: TypedT) {
export function applyDescriptionLocalizedBuilder<T extends BuilderWithDescription>(builder: T, key: LocalePrefixKey) {
const result = getLocalizedData(key);
return builder.setDescription(result.value).setDescriptionLocalizations(result.localizations);
}
Expand All @@ -126,16 +88,16 @@ export function applyDescriptionLocalizedBuilder<T extends BuilderWithDescriptio
*/
export function applyLocalizedBuilder<T extends BuilderWithNameAndDescription>(
builder: T,
...params: [root: LocalePrefixKey] | [name: TypedT, description: TypedT]
...params: [root: LocalePrefixKey] | [name: LocalePrefixKey, description: LocalePrefixKey]
): T {
const [localeName, localeDescription] = params.length === 1 ? [`${params[0]}Name` as TypedT, `${params[0]}Description` as TypedT] : params;
const [localeName, localeDescription] = params.length === 1 ? [`${params[0]}Name` as const, `${params[0]}Description` as const] : params;

applyNameLocalizedBuilder(builder, localeName);
applyDescriptionLocalizedBuilder(builder, localeDescription);
return builder;
}

export function createSelectMenuChoiceName<V extends NonNullObject>(key: TypedT, value?: V): createSelectMenuChoiceName.Result<V> {
export function createSelectMenuChoiceName<V extends NonNullObject>(key: LocalePrefixKey, value?: V): createSelectMenuChoiceName.Result<V> {
const result = getLocalizedData(key);
return {
...value,
Expand Down
1 change: 1 addition & 0 deletions packages/i18next-type-generator/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @wolfstar/i18next-type-generator
54 changes: 54 additions & 0 deletions packages/i18next-type-generator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# `@wolfstar/i18next-type-generator`

A fast and modern type augmentation generator for the [`@wolfstar/i18next-backend`](https://www.npmjs.com/package/@wolfstar/i18next-backend) filesystem-based [`i18next`](https://www.npmjs.com/package/i18next) backend for Node.js.

## Usage

```bash
$ i18next-type-generator [options] [source] [destination]

# Arguments:
# source The directory to generate types from (default: "./src/locales/en-US/")
# destination The directory to generate types to (default: "./src/@types/i18next.d.ts")
#
# Options:
# -V, --version output the version number
# -v, --verbose Verbose output
# -i, --indentation <value> Indentation for generated output (number of spaces or "tabs", default: tabs)
# --no-prettier Disable prettier
# -h, --help display help for command
Comment thread
RedStar071 marked this conversation as resolved.
```

This CLI tool generates a `.d.ts` file with the following structure:

```typescript
// This file is automatically generated, do not edit it.
/* eslint-disable */
/* oxlint-disable */
// oxfmt-ignore
// prettier-ignore
import 'i18next';

// oxfmt-ignore
// prettier-ignore
declare module 'i18next' {
interface CustomTypeOptions {
resources: {
'commands/choice': {
name: 'choice';
description: 'Get a random value from a set of choices';
// ...
};
// ...
};
}
}
```

The output includes `eslint-disable` / `oxlint-disable` comments to suppress linting, and `oxfmt-ignore` / `prettier-ignore` comments before each top-level statement to prevent formatters from rewriting the generated file. These ignore directives are always emitted; the `--no-oxfmt` and `--no-prettier` flags only affect the internal formatting pass used to produce the resource block, not the emitted ignore comments.

The command reads the JSON files inside a directory writes their contents into the `.d.ts` file. This is needed because `typeof import(pathToJSON)` requires the JSON files to be included in `tsconfig.json`, which may be undesirable, and because it types the keys and their inferred types, but does not load the exact values, which breaks i18next's ability to extract arguments from strings.

You can control indentation with `-i` / `--indentation` (a space count or `tabs`). `prettier` is also used under the hood to format output before writing to a file; opt out with `--no-prettier` if you want to use a different tool.

> **Note**: If you want to customize `i18next`'s `CustomTypeOptions` to add [extra options](https://www.i18next.com/overview/typescript), create a different file, TypeScript will merge the two of them.
71 changes: 71 additions & 0 deletions packages/i18next-type-generator/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"name": "@wolfstar/i18next-type-generator",
"version": "3.0.1",
"description": "A fast utility that generates the TypeScript augmentation for i18next.",
"keywords": [
"api",
"discord",
"http",
"i18next",
"pnpm",
"ts",
"typescript",
"wolfstar"
],
"homepage": "https://github.com/wolfstar-project/stars-components/tree/main/packages/i18next-type-generator",
"bugs": {
"url": "https://github.com/wolfstar-project/stars-components/issues"
},
"license": "Apache-2.0",
"author": "@wolfstar",
"repository": {
"type": "git",
"url": "git+https://github.com/wolfstar-project/stars-components.git",
"directory": "packages/i18next-type-generator"
},
Comment thread
RedStar071 marked this conversation as resolved.
"bin": {
"i18next-type-generator": "./dist/cli.js"
},
"files": [
"dist/"
],
"type": "module",
"sideEffects": false,
"main": "dist/cli.js",
"exports": {
".": {
"default": "./dist/cli.js"
}
},
"publishConfig": {
"access": "public"
},
"scripts": {
"test": "vitest run",
"build": "tsdown --config-loader unrun",
"watch": "tsdown --config-loader unrun --watch",
"typecheck": "tsc -p ../../tsconfig.json",
"lint": "oxlint src --fix",
"prepack": "pnpm run build"
},
"dependencies": {
"citty": "^0.1.6",
"colorette": "^2.0.20",
"tslib": "^2.8.1"
},
"devDependencies": {
"@types/node": "22.15.21",
"@vitest/coverage-v8": "^4.1.8",
"i18next": "^25.10.10",
"tsdown": "^0.22.2",
"typescript": "~5.8.3",
"vitest": "^4.1.8"
},
"optionalDependencies": {
"oxfmt": "^0.53.0",
"prettier": "^3.5.3"
},
"engines": {
"node": ">=20"
}
}
Loading
Loading