Skip to content
Draft
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:
- '@skyra/http-framework'
- '@skyra/http-framework-i18n'
- '@skyra/i18next-backend'
- '@skyra/i18next-type-generator'
- '@skyra/logger'
- '@skyra/shared-http-pieces'
- '@skyra/start-banner'
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/@skyra/http-framework?color=crimson&logo=npm&style=flat-square&label=@skyra/http-framework)](https://www.npmjs.com/package/@skyra/http-framework)
[![npm](https://img.shields.io/npm/v/@skyra/http-framework-i18n?color=crimson&logo=npm&style=flat-square&label=@skyra/http-framework-i18n)](https://www.npmjs.com/package/@skyra/http-framework-i18n)
[![npm](https://img.shields.io/npm/v/@skyra/i18next-backend?color=crimson&logo=npm&style=flat-square&label=@skyra/i18next-backend)](https://www.npmjs.com/package/@skyra/i18next-backend)
[![npm](https://img.shields.io/npm/v/@skyra/i18next-type-generator?color=crimson&logo=npm&style=flat-square&label=@skyra/i18next-type-generator)](https://www.npmjs.com/package/@skyra/i18next-type-generator)
[![npm](https://img.shields.io/npm/v/@skyra/logger?color=crimson&logo=npm&style=flat-square&label=@skyra/logger)](https://www.npmjs.com/package/@skyra/logger)
[![npm](https://img.shields.io/npm/v/@skyra/shared-http-pieces?color=crimson&logo=npm&style=flat-square&label=@skyra/shared-http-pieces)](https://www.npmjs.com/package/@skyra/shared-http-pieces)
[![npm](https://img.shields.io/npm/v/@skyra/start-banner?color=crimson&logo=npm&style=flat-square&label=@skyra/start-banner)](https://www.npmjs.com/package/@skyra/start-banner)
Expand Down
2 changes: 2 additions & 0 deletions packages/http-framework-i18n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ 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
Expand Down
6 changes: 3 additions & 3 deletions packages/http-framework-i18n/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
"@discordjs/collection": "^1.5.2",
"@sapphire/utilities": "^3.13.0",
"@skyra/i18next-backend": "workspace:^",
"discord-api-types": "^0.37.52",
"i18next": "^22.5.1",
"tslib": "^2.6.1"
"discord-api-types": "^0.37.54",
"i18next": "^23.4.4",
"tslib": "^2.6.2"
},
"repository": {
"type": "git",
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 '@skyra/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 @@ -82,8 +81,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
2 changes: 1 addition & 1 deletion packages/http-framework/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"@sapphire/result": "^2.6.4",
"@sapphire/utilities": "^3.13.0",
"@vladfrangu/async_event_emitter": "^2.2.2",
"discord-api-types": "^0.37.52"
"discord-api-types": "^0.37.54"
},
"devDependencies": {
"@favware/cliff-jumper": "^2.1.1",
Expand Down
4 changes: 2 additions & 2 deletions packages/i18next-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@
"check-update": "cliff-jumper --dry-run"
},
"dependencies": {
"tslib": "^2.6.1"
"tslib": "^2.6.2"
},
"devDependencies": {
"@favware/cliff-jumper": "^2.1.1",
"@types/node": "^18.17.4",
"i18next": "^22.5.1",
"i18next": "^23.4.4",
"tsup": "^7.2.0",
"typescript": "^5.1.6"
},
Expand Down
4 changes: 4 additions & 0 deletions packages/i18next-type-generator/.cliff-jumperrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name: i18next-type-generator
org: skyra
install: true
packagePath: packages/i18next-type-generator
3 changes: 3 additions & 0 deletions packages/i18next-type-generator/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Changelog

All notable changes to this project will be documented in this file.
45 changes: 45 additions & 0 deletions packages/i18next-type-generator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# `@skyra/i18next-type-generator`

A fast and modern type augmentation generator for the [`@skyra/i18next-backend`](https://www.npmjs.com/package/@skyra/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
# --no-prettier Disable prettier
# -h, --help display help for command
```

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

```typescript
// This file is automatically generated, do not edit it.
import 'i18next';

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

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.

This utility does not provide formatting options, so `prettier` is used under the hood to format it before writing to a file, you may opt-out using `--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.
63 changes: 63 additions & 0 deletions packages/i18next-type-generator/cliff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
[changelog]
header = """
# Changelog

All notable changes to this project will be documented in this file.\n
"""
body = """
{% if version %}\
# [{{ version | trim_start_matches(pat="v") }}]\
{% if previous %}\
{% if previous.version %}\
(https://github.com/skyra-project/archid-components/compare/{{ previous.version }}...{{ version }})\
{% else %}\
(https://github.com/skyra-project/archid-components/tree/{{ version }})\
{% endif %}\
{% endif %} \
- ({{ timestamp | date(format="%Y-%m-%d") }})
{% else %}\
# [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
## {{ group | upper_first }}
{% for commit in commits %}
- {% if commit.scope %}\
**{{commit.scope}}:** \
{% endif %}\
{{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/skyra-project/archid-components/commit/{{ commit.id }}))\
{% if commit.breaking %}\
{% for breakingChange in commit.footers %}\
\n{% raw %} {% endraw %}- 💥 **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\
{% endfor %}\
{% endif %}\
{% endfor %}
{% endfor %}\n
"""
trim = true
footer = ""

[git]
conventional_commits = true
filter_unconventional = true
commit_parsers = [
{ message = "^feat", group = "🚀 Features"},
{ message = "^fix", group = "🐛 Bug Fixes"},
{ message = "^docs", group = "📝 Documentation"},
{ message = "^perf", group = "🏃 Performance"},
{ message = "^refactor", group = "🏠 Refactor"},
{ message = "^typings", group = "⌨️ Typings"},
{ message = "^types", group = "⌨️ Typings"},
{ message = ".*deprecated", body = ".*deprecated", group = "🚨 Deprecation"},
{ message = "^revert", skip = true},
{ message = "^style", group = "🪞 Styling"},
{ message = "^test", group = "🧪 Testing"},
{ message = "^chore", skip = true},
{ message = "^ci", skip = true},
{ message = "^build", skip = true},
{ body = ".*security", group = "🛡️ Security"},
]
filter_commits = true
tag_pattern = "@skyra/i18next-type-generator@[0-9]*"
ignore_tags = ""
topo_order = false
sort_commits = "newest"
Loading