diff --git a/.npm-deprecaterc.yml b/.npm-deprecaterc.yml index 23bc536..825870c 100644 --- a/.npm-deprecaterc.yml +++ b/.npm-deprecaterc.yml @@ -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' diff --git a/README.md b/README.md index fb125f5..bd453d4 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/packages/http-framework-i18n/README.md b/packages/http-framework-i18n/README.md index 3fca8ef..0dec440 100644 --- a/packages/http-framework-i18n/README.md +++ b/packages/http-framework-i18n/README.md @@ -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); @@ -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 }); ``` diff --git a/packages/http-framework-i18n/package.json b/packages/http-framework-i18n/package.json index b608d04..d271978 100644 --- a/packages/http-framework-i18n/package.json +++ b/packages/http-framework-i18n/package.json @@ -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": { diff --git a/packages/http-framework-i18n/src/index.ts b/packages/http-framework-i18n/src/index.ts index 6eb1321..38dab66 100644 --- a/packages/http-framework-i18n/src/index.ts +++ b/packages/http-framework-i18n/src/index.ts @@ -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; - - (key: TypedT, options?: TOptionsBase | string): TReturn; - (key: TypedT, defaultValue: TReturn, options?: TOptionsBase | string): TReturn; - (key: TypedFT, options?: TOptions): TReturn; - (key: TypedFT, defaultValue: TReturn, options?: TOptions): TReturn; - } -} diff --git a/packages/http-framework-i18n/src/lib/functions.ts b/packages/http-framework-i18n/src/lib/functions.ts deleted file mode 100644 index aaf77e7..0000000 --- a/packages/http-framework-i18n/src/lib/functions.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { NonNullObject } from '@sapphire/utilities'; -import type { TypedFT, TypedT } from './types.js'; - -export function T(k: string): TypedT { - return k as TypedT; -} - -export function FT(k: string): TypedFT { - return k as TypedFT; -} diff --git a/packages/http-framework-i18n/src/lib/registry.ts b/packages/http-framework-i18n/src/lib/registry.ts index 4b580b9..9f2a0fc 100644 --- a/packages/http-framework-i18n/src/lib/registry.ts +++ b/packages/http-framework-i18n/src/lib/registry.ts @@ -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'; @@ -85,8 +84,7 @@ async function loadLocale(directory: string, ns: string) { } } -const fixedCache = new Collection(); -export function getT(locale: LocaleString): TFunction<'translation', undefined, 'translation'> { +export function getT(locale: LocaleString, namespace?: Ns): TFunction { if (!loadedLocales.has(locale)) throw new ReferenceError(`Invalid language (${locale})`); - return fixedCache.ensure(locale, () => getFixedT(locale)); + return getFixedT(locale, namespace); } diff --git a/packages/http-framework-i18n/src/lib/types.ts b/packages/http-framework-i18n/src/lib/types.ts deleted file mode 100644 index a02e727..0000000 --- a/packages/http-framework-i18n/src/lib/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { NonNullObject } from '@sapphire/utilities'; - -export type TypedT = string & { __type__: TCustom }; - -export type TypedFT = string & { __args__: TArgs; __return__: TReturn }; - -export interface Value { - value: T; -} - -export interface Values { - values: readonly T[]; - count: number; -} - -export interface Difference { - previous: T; - next: T; -} - -export type LocalePrefixKey = `commands/${string}:${string}`; diff --git a/packages/http-framework-i18n/src/lib/utils.ts b/packages/http-framework-i18n/src/lib/utils.ts index dde30df..6a2d0e8 100644 --- a/packages/http-framework-i18n/src/lib/utils.ts +++ b/packages/http-framework-i18n/src/lib/utils.ts @@ -2,11 +2,12 @@ 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, '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; @@ -14,8 +15,8 @@ export function getSupportedUserLanguageName(interaction: Interaction): LocaleSt return 'en-US'; } -export function getSupportedUserLanguageT(interaction: Interaction): TFunction { - return getT(getSupportedUserLanguageName(interaction)); +export function getSupportedUserLanguageT(interaction: Interaction, namespace?: Ns): TFunction { + return getT(getSupportedUserLanguageName(interaction), namespace); } export function getSupportedLanguageName(interaction: Interaction): LocaleString { @@ -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(interaction: Interaction, key: TypedT, options?: TOptionsBase | string): TReturn; -export function resolveUserKey( - interaction: Interaction, - key: TypedT, - defaultValue: TReturn, - options?: TOptionsBase | string -): TReturn; -export function resolveUserKey( - interaction: Interaction, - key: TypedFT, - options?: TOptions -): TReturn; -export function resolveUserKey( - interaction: Interaction, - key: TypedFT, - defaultValue: TReturn, - options?: TOptions -): TReturn; -export function resolveUserKey(interaction: Interaction, ...args: [any, any, any?]) { - return getSupportedUserLanguageT(interaction)(...args); -} - -export function resolveKey(interaction: Interaction, key: TypedT, options?: TOptionsBase | string): TReturn; -export function resolveKey(interaction: Interaction, key: TypedT, defaultValue: TReturn, options?: TOptionsBase | string): TReturn; -export function resolveKey( - interaction: Interaction, - key: TypedFT, - options?: TOptions -): TReturn; -export function resolveKey( - interaction: Interaction, - key: TypedFT, - defaultValue: TReturn, - options?: TOptions -): TReturn; -export function resolveKey(interaction: Interaction, ...args: [any, any, any?]) { - return getSupportedLanguageT(interaction)(...args); +export function getSupportedLanguageT(interaction: Interaction, namespace?: Ns): TFunction { + return getT(getSupportedLanguageName(interaction), namespace); } const getLocales = lazy(() => new Collection([...loadedLocales].map((locale) => [locale, getT(locale)]))); @@ -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(); @@ -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(builder: T, key: TypedT) { +export function applyNameLocalizedBuilder(builder: T, key: LocalePrefixKey) { const result = getLocalizedData(key); return builder.setName(result.value).setNameLocalizations(result.localizations); } @@ -110,7 +72,7 @@ export function applyNameLocalizedBuilder(builder: T, * @param key The key to get the localizations from. * @returns The updated builder. */ -export function applyDescriptionLocalizedBuilder(builder: T, key: TypedT) { +export function applyDescriptionLocalizedBuilder(builder: T, key: LocalePrefixKey) { const result = getLocalizedData(key); return builder.setDescription(result.value).setDescriptionLocalizations(result.localizations); } @@ -126,16 +88,16 @@ export function applyDescriptionLocalizedBuilder( 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(key: TypedT, value?: V): createSelectMenuChoiceName.Result { +export function createSelectMenuChoiceName(key: LocalePrefixKey, value?: V): createSelectMenuChoiceName.Result { const result = getLocalizedData(key); return { ...value, diff --git a/packages/i18next-type-generator/CHANGELOG.md b/packages/i18next-type-generator/CHANGELOG.md new file mode 100644 index 0000000..a1bcfb7 --- /dev/null +++ b/packages/i18next-type-generator/CHANGELOG.md @@ -0,0 +1 @@ +# @wolfstar/i18next-type-generator diff --git a/packages/i18next-type-generator/README.md b/packages/i18next-type-generator/README.md new file mode 100644 index 0000000..0cda5b4 --- /dev/null +++ b/packages/i18next-type-generator/README.md @@ -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 Indentation for generated output (number of spaces or "tabs", default: tabs) +# --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. +/* 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. diff --git a/packages/i18next-type-generator/package.json b/packages/i18next-type-generator/package.json new file mode 100644 index 0000000..49b5ccd --- /dev/null +++ b/packages/i18next-type-generator/package.json @@ -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" + }, + "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" + } +} diff --git a/packages/i18next-type-generator/src/cli.ts b/packages/i18next-type-generator/src/cli.ts new file mode 100644 index 0000000..aec6cc1 --- /dev/null +++ b/packages/i18next-type-generator/src/cli.ts @@ -0,0 +1,84 @@ +#!/usr/bin/env node + +import { defineCommand, runCommand, showUsage } from 'citty'; +import { readFile } from 'node:fs/promises'; +import { generate } from './generate.js'; + +const packageFile = new URL('../package.json', import.meta.url); +const packageJson = JSON.parse(await readFile(packageFile, 'utf-8')); + +const parseIndentation = (value: string): string => { + if (value === 'tabs') return '\t'; + + const parsed = Number(value); + if (Number.isNaN(parsed)) throw new Error('The indentation must be a number or "tabs"'); + return ' '.repeat(parsed); +}; + +const main = defineCommand({ + meta: { + name: 'i18next-type-generator', + version: packageJson.version, + description: packageJson.description + }, + args: { + source: { + type: 'positional', + description: 'The directory to generate types from', + default: './src/locales/en-US/' + }, + destination: { + type: 'positional', + description: 'The directory to generate types to', + default: './src/@types/i18next.d.ts' + }, + indentation: { + type: 'string', + description: 'The indentation to use', + alias: 'i', + default: 'tabs' + }, + verbose: { + type: 'boolean', + description: 'Verbose output', + alias: 'v' + }, + prettier: { + type: 'boolean', + description: 'Format output with prettier', + default: true + }, + oxfmt: { + type: 'boolean', + description: 'Format output with oxfmt', + default: true + } + }, + async run({ args }) { + await generate([args.source, args.destination], { + verbose: args.verbose, + oxfmt: args.oxfmt, + prettier: args.prettier, + indentation: parseIndentation(args.indentation) + }); + } +}); + +const rawArgs = process.argv.slice(2); + +if (rawArgs.includes('--help') || rawArgs.includes('-h')) { + await showUsage(main); + process.exit(0); +} + +if (rawArgs.includes('--version')) { + console.log(packageJson.version); + process.exit(0); +} + +try { + await runCommand(main, { rawArgs }); +} catch (error) { + console.error(error instanceof Error ? error.message : error); + process.exit(1); +} diff --git a/packages/i18next-type-generator/src/generate.ts b/packages/i18next-type-generator/src/generate.ts new file mode 100644 index 0000000..00fbfff --- /dev/null +++ b/packages/i18next-type-generator/src/generate.ts @@ -0,0 +1,159 @@ +import { gray, green, italic, yellow } from 'colorette'; +import { mkdir, opendir, readFile, writeFile } from 'node:fs/promises'; +import { dirname, join, resolve } from 'node:path'; +import { inspect } from 'node:util'; + +const ci = 'CI' in process.env && process.env.CI !== 'false'; + +const FileIcon = ci ? '๐Ÿ“„' : '\uE628'; +const DirectoryIcon = ci ? '๐Ÿ“‚' : '\uF413'; + +const GENERATED_FILE_HEADER = ['// This file is automatically generated, do not edit it.', '/* eslint-disable */', '/* oxlint-disable */'] as const; + +const FORMATTER_IGNORE = ['// oxfmt-ignore', '// prettier-ignore'] as const; + +/** + * Inject formatter and linter ignore directives into the already-formatted output. + * + * The file-level `eslint-disable` / `oxlint-disable` comments suppress all lint rules, while the + * per-statement `oxfmt-ignore` / `prettier-ignore` comments protect the two top-level statements + * (the `i18next` import and the `declare module` block), since neither formatter supports a + * whole-file inline ignore. + * @param source The formatted generated source. + */ +export function annotateGeneratedFile(source: string): string { + const ignore = FORMATTER_IGNORE.join('\n'); + const annotated = source + .replace(/^(import\s+['"]i18next['"])/m, `${ignore}\n$1`) + .replace(/^(declare\s+module\s+['"]i18next['"])/m, `${ignore}\n$1`); + + return `${GENERATED_FILE_HEADER.join('\n')}\n${annotated}`; +} + +/** + * Recursively walk through the directory and generate the types. + * @param path The path to walk through. + * @param namespace The namespace to use. + */ +async function recurse(lines: string[], path: string, namespace: string, options: GenerateOptions) { + const indent = options.indentation.repeat(3); + + if (options.verbose) console.log(gray(`Reading directory ${DirectoryIcon} ${green(path)}...`)); + + const entries = []; + for await (const dirent of await opendir(path)) { + entries.push(dirent); + } + entries.sort((a, b) => a.name.localeCompare(b.name)); + + for (const dirent of entries) { + const file = join(path, dirent.name); + if (dirent.isFile()) { + if (!dirent.name.endsWith('.json')) continue; + if (options.verbose) console.log(gray(`Processing ${FileIcon} ${green(file)}...`)); + + const name = dirent.name.slice(0, -5); + const key = namespace ? `'${namespace}/${name}'` : name; + const data = JSON.stringify(JSON.parse(await readFile(file, 'utf8')), undefined, options.indentation); + lines.push(`${indent}${key}: ${data.replaceAll('\n', `\n${indent}`)};`); + } else if (dirent.isDirectory()) { + await recurse(lines, file, namespace ? `${namespace}/${dirent.name}` : dirent.name, options); + } + } +} + +async function formatWithOxfmt(source: string, destination: string, options: GenerateOptions): Promise { + if (!options.oxfmt) return; + + try { + if (options.verbose) console.log(gray('Loading oxfmt...')); + + const { format } = await import('oxfmt'); + const { code, errors } = await format(destination, source); + if (errors.length > 0) { + throw new Error(errors.map((error) => error.message).join('\n')); + } + + if (options.verbose) console.log(gray('Formatting with oxfmt.')); + + return code; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.warn( + yellow( + options.verbose + ? `Failed to format with oxfmt: ${message}. Continuing with unformatted output.` + : 'Failed to format with oxfmt; install it as a dependency or use --no-oxfmt. Continuing with unformatted output.' + ) + ); + } + + return undefined; +} + +async function formatWithPrettier(source: string, destination: string, options: GenerateOptions): Promise { + if (!options.prettier) return; + + try { + if (options.verbose) console.log(gray('Loading prettier...')); + + const { resolveConfig, format } = await import('prettier'); + const config = await resolveConfig(destination); + if (options.verbose) console.log(gray(`Formatting with prettier config: ${inspect(config, { colors: true })}`)); + + return await format(source, { ...config, filepath: destination }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.warn( + yellow( + options.verbose + ? `Failed to format with prettier: ${message}. Continuing with unformatted output.` + : 'Failed to format with prettier; install it as a dependency or use --no-prettier. Continuing with unformatted output.' + ) + ); + } + + return undefined; +} + +async function formatSource(source: string, destination: string, options: GenerateOptions) { + return (await formatWithOxfmt(source, destination, options)) ?? (await formatWithPrettier(source, destination, options)) ?? source; +} + +export async function generate([source, destination]: string[], options: GenerateOptions) { + const sourceDirectory = resolve(source); + const destinationFile = resolve(destination); + const i1 = options.indentation; + const i2 = i1.repeat(2); + + if (options.verbose) { + const lines = [ + `Source: ${DirectoryIcon} ${green(sourceDirectory)}...`, + `Output: ${FileIcon} ${green(destinationFile)}...`, + '', + 'Options:', + ` - Verbose: ${green(options.verbose ? 'yes' : 'no')}`, + ` - Indentation: ${green(JSON.stringify(options.indentation))}` + ]; + console.log(italic(gray(lines.join('\n')))); + } + + const lines = ["import 'i18next';", '', "declare module 'i18next' {", `${i1}interface CustomTypeOptions {`, `${i2}resources: {`]; + + await recurse(lines, sourceDirectory, '', options); + lines.push(`${i2}};`, `${i1}}`, '}', ''); + + let generatedSource = lines.join('\n'); + generatedSource = await formatSource(generatedSource, destinationFile, options); + generatedSource = annotateGeneratedFile(generatedSource); + + await mkdir(dirname(destinationFile), { recursive: true }); + await writeFile(destinationFile, generatedSource, 'utf8'); +} + +interface GenerateOptions { + verbose: boolean; + oxfmt: boolean; + prettier: boolean; + indentation: string; +} diff --git a/packages/i18next-type-generator/tests/cli.test.ts b/packages/i18next-type-generator/tests/cli.test.ts new file mode 100644 index 0000000..7fa1500 --- /dev/null +++ b/packages/i18next-type-generator/tests/cli.test.ts @@ -0,0 +1,119 @@ +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +class ProcessExit extends Error { + constructor(public readonly code: number) { + super(`process.exit(${code})`); + } +} + +async function createFixtureDirectory() { + const tempDirectory = await mkdtemp(join(tmpdir(), 'i18next-type-generator-cli-')); + const sourceDirectory = join(tempDirectory, 'locales', 'en-US'); + const destinationFile = join(tempDirectory, 'output', 'i18next.d.ts'); + + await mkdir(join(sourceDirectory, 'commands'), { recursive: true }); + await writeFile(join(sourceDirectory, 'commands', 'shared.json'), JSON.stringify({ hello: 'world' })); + + return { tempDirectory, sourceDirectory, destinationFile }; +} + +async function runCli(argv: string[]) { + vi.resetModules(); + + const originalArgv = process.argv; + process.argv = ['node', 'cli.js', ...argv]; + + const logs: string[] = []; + const errors: string[] = []; + const logSpy = vi.spyOn(console, 'log').mockImplementation((...args) => { + logs.push(args.map(String).join(' ')); + }); + const errorSpy = vi.spyOn(console, 'error').mockImplementation((...args) => { + errors.push(args.map(String).join(' ')); + }); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => { + throw new ProcessExit(code ?? 0); + }); + + try { + await import('../src/cli.ts'); + return { code: 0, stdout: logs.join('\n'), stderr: errors.join('\n') }; + } catch (error) { + if (error instanceof ProcessExit) { + return { code: error.code, stdout: logs.join('\n'), stderr: errors.join('\n') }; + } + + throw error; + } finally { + process.argv = originalArgv; + logSpy.mockRestore(); + errorSpy.mockRestore(); + exitSpy.mockRestore(); + } +} + +describe('cli', () => { + let fixtureDirectory: string; + let sourceDirectory: string; + let destinationFile: string; + + beforeEach(async () => { + ({ tempDirectory: fixtureDirectory, sourceDirectory, destinationFile } = await createFixtureDirectory()); + }); + + afterEach(async () => { + await rm(fixtureDirectory, { recursive: true, force: true }); + }); + + test('GIVEN --version THEN prints the package version', async () => { + const packageJson = JSON.parse(await readFile(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8')); + const { code, stdout } = await runCli(['--version']); + + expect(code).toBe(0); + expect(stdout.trim()).toBe(packageJson.version); + }); + + test('GIVEN --help THEN exits successfully', async () => { + const { code } = await runCli(['--help']); + + expect(code).toBe(0); + }); + + test('GIVEN source and destination THEN generates types', async () => { + const { code, stderr } = await runCli([sourceDirectory, destinationFile, '--no-oxfmt', '--no-prettier']); + + expect(code).toBe(0); + expect(stderr).toBe(''); + + const output = await readFile(destinationFile, 'utf8'); + expect(output).toContain('commands/shared'); + }); + + test('GIVEN invalid indentation THEN exits with an error', async () => { + const { code, stderr } = await runCli([sourceDirectory, destinationFile, '--indentation', 'invalid', '--no-oxfmt', '--no-prettier']); + + expect(code).toBe(1); + expect(stderr).toContain('indentation'); + }); + + test('GIVEN tabs indentation THEN generates types', async () => { + const { code } = await runCli([sourceDirectory, destinationFile, '--indentation', 'tabs', '--no-oxfmt', '--no-prettier']); + + expect(code).toBe(0); + + const output = await readFile(destinationFile, 'utf8'); + expect(output).toContain('\tinterface CustomTypeOptions'); + }); + + test('GIVEN numeric indentation THEN generates types', async () => { + const { code } = await runCli([sourceDirectory, destinationFile, '--indentation', '2', '--no-oxfmt', '--no-prettier']); + + expect(code).toBe(0); + + const output = await readFile(destinationFile, 'utf8'); + expect(output).toContain(' interface CustomTypeOptions'); + }); +}); diff --git a/packages/i18next-type-generator/tests/generate-formatter-fallback.test.ts b/packages/i18next-type-generator/tests/generate-formatter-fallback.test.ts new file mode 100644 index 0000000..7545cb2 --- /dev/null +++ b/packages/i18next-type-generator/tests/generate-formatter-fallback.test.ts @@ -0,0 +1,46 @@ +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +vi.mock('oxfmt', () => ({ + format: async () => ({ code: '', errors: [{ message: 'format failed' }] }) +})); + +const { generate } = await import('../src/generate.js'); + +describe('generate formatter fallbacks', () => { + let tempDirectory: string; + let sourceDirectory: string; + let destinationFile: string; + + beforeEach(async () => { + tempDirectory = await mkdtemp(join(tmpdir(), 'i18next-type-generator-fallback-')); + sourceDirectory = join(tempDirectory, 'locales', 'en-US'); + destinationFile = join(tempDirectory, 'output', 'i18next.d.ts'); + + await mkdir(join(sourceDirectory, 'commands'), { recursive: true }); + await writeFile(join(sourceDirectory, 'commands', 'shared.json'), JSON.stringify({ hello: 'world' })); + }); + + afterEach(async () => { + await rm(tempDirectory, { recursive: true, force: true }); + }); + + test('GIVEN oxfmt errors THEN falls back to unformatted output', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + await generate([sourceDirectory, destinationFile], { + verbose: false, + oxfmt: true, + prettier: false, + indentation: '\t' + }); + + const output = await readFile(destinationFile, 'utf8'); + + expect(output).toContain('commands/shared'); + expect(warnSpy).toHaveBeenCalled(); + + warnSpy.mockRestore(); + }); +}); diff --git a/packages/i18next-type-generator/tests/generate-prettier-fallback.test.ts b/packages/i18next-type-generator/tests/generate-prettier-fallback.test.ts new file mode 100644 index 0000000..e687237 --- /dev/null +++ b/packages/i18next-type-generator/tests/generate-prettier-fallback.test.ts @@ -0,0 +1,49 @@ +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +vi.mock('prettier', () => ({ + resolveConfig: async () => ({}), + format: async () => { + throw new Error('format failed'); + } +})); + +const { generate } = await import('../src/generate.js'); + +describe('generate prettier fallbacks', () => { + let tempDirectory: string; + let sourceDirectory: string; + let destinationFile: string; + + beforeEach(async () => { + tempDirectory = await mkdtemp(join(tmpdir(), 'i18next-type-generator-prettier-')); + sourceDirectory = join(tempDirectory, 'locales', 'en-US'); + destinationFile = join(tempDirectory, 'output', 'i18next.d.ts'); + + await mkdir(join(sourceDirectory, 'commands'), { recursive: true }); + await writeFile(join(sourceDirectory, 'commands', 'shared.json'), JSON.stringify({ hello: 'world' })); + }); + + afterEach(async () => { + await rm(tempDirectory, { recursive: true, force: true }); + }); + + test('GIVEN prettier errors THEN falls back to unformatted output', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + await generate([sourceDirectory, destinationFile], { + verbose: false, + oxfmt: false, + prettier: true, + indentation: '\t' + }); + + const output = await readFile(destinationFile, 'utf8'); + + expect(output).toContain('commands/shared'); + expect(warnSpy).toHaveBeenCalled(); + + warnSpy.mockRestore(); + }); +}); diff --git a/packages/i18next-type-generator/tests/generate.test.ts b/packages/i18next-type-generator/tests/generate.test.ts new file mode 100644 index 0000000..d8a0fcc --- /dev/null +++ b/packages/i18next-type-generator/tests/generate.test.ts @@ -0,0 +1,135 @@ +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { annotateGeneratedFile, generate } from '../src/generate.js'; + +const formatted = ["import 'i18next';", '', "declare module 'i18next' {", '\tinterface CustomTypeOptions {}', '}', ''].join('\n'); + +const defaultOptions = { + verbose: false, + oxfmt: false, + prettier: false, + indentation: '\t' +} as const; + +async function writeFixture(sourceDirectory: string) { + await mkdir(join(sourceDirectory, 'commands'), { recursive: true }); + await writeFile( + join(sourceDirectory, 'commands', 'shared.json'), + JSON.stringify({ + hello: 'world', + count: 1 + }) + ); + await writeFile(join(sourceDirectory, 'readme.txt'), 'not json'); +} + +describe('annotateGeneratedFile', () => { + test('GIVEN formatted source THEN prepends the generated file header', () => { + const result = annotateGeneratedFile(formatted); + + expect( + result.startsWith(['// This file is automatically generated, do not edit it.', '/* eslint-disable */', '/* oxlint-disable */'].join('\n')) + ).toBe(true); + }); + + test('GIVEN formatted source THEN inserts formatter ignores before the import statement', () => { + const result = annotateGeneratedFile(formatted); + + expect(result).toContain(['// oxfmt-ignore', '// prettier-ignore', "import 'i18next';"].join('\n')); + }); + + test('GIVEN formatted source THEN inserts formatter ignores before the declare module statement', () => { + const result = annotateGeneratedFile(formatted); + + expect(result).toContain(['// oxfmt-ignore', '// prettier-ignore', "declare module 'i18next' {"].join('\n')); + }); +}); + +describe('generate', () => { + let tempDirectory: string; + let sourceDirectory: string; + let destinationFile: string; + + beforeEach(async () => { + tempDirectory = await mkdtemp(join(tmpdir(), 'i18next-type-generator-')); + sourceDirectory = join(tempDirectory, 'locales', 'en-US'); + destinationFile = join(tempDirectory, 'output', 'i18next.d.ts'); + await writeFixture(sourceDirectory); + }); + + afterEach(async () => { + await rm(tempDirectory, { recursive: true, force: true }); + }); + + test('GIVEN locale JSON files THEN writes augmented i18next types', async () => { + await generate([sourceDirectory, destinationFile], defaultOptions); + + const output = await readFile(destinationFile, 'utf8'); + + expect(output).toContain('commands/shared'); + expect(output).toContain('hello'); + expect(output).toContain('world'); + expect(output).toContain("import 'i18next'"); + expect(output).toContain('interface CustomTypeOptions'); + }); + + test('GIVEN nested locale directories THEN namespaces keys', async () => { + await mkdir(join(sourceDirectory, 'guilds'), { recursive: true }); + await writeFile(join(sourceDirectory, 'guilds', 'settings.json'), JSON.stringify({ enabled: true })); + + await generate([sourceDirectory, destinationFile], defaultOptions); + + const output = await readFile(destinationFile, 'utf8'); + + expect(output).toContain('guilds/settings'); + expect(output).toContain('enabled'); + }); + + test('GIVEN space indentation THEN indents generated resources', async () => { + await generate([sourceDirectory, destinationFile], { + ...defaultOptions, + indentation: ' ' + }); + + const output = await readFile(destinationFile, 'utf8'); + + expect(output).toContain(' interface CustomTypeOptions'); + expect(output).toContain(' resources:'); + }); + + test('GIVEN verbose mode THEN logs progress without failing', async () => { + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + await generate([sourceDirectory, destinationFile], { + ...defaultOptions, + verbose: true + }); + + expect(logSpy).toHaveBeenCalled(); + logSpy.mockRestore(); + }); + + test('GIVEN oxfmt enabled THEN formats output', async () => { + await generate([sourceDirectory, destinationFile], { + ...defaultOptions, + oxfmt: true + }); + + const output = await readFile(destinationFile, 'utf8'); + + expect(output).toContain('commands/shared'); + }); + + test('GIVEN prettier enabled THEN formats output', async () => { + await generate([sourceDirectory, destinationFile], { + ...defaultOptions, + prettier: true + }); + + const output = await readFile(destinationFile, 'utf8'); + + expect(output).toContain('commands/shared'); + }); +}); diff --git a/packages/i18next-type-generator/tsdown.config.ts b/packages/i18next-type-generator/tsdown.config.ts new file mode 100644 index 0000000..d78c1df --- /dev/null +++ b/packages/i18next-type-generator/tsdown.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'tsdown'; +import { createTsdownOptions } from '../../scripts/tsdown.config'; + +export default defineConfig({ + ...createTsdownOptions({ + cjsOptions: { disabled: true }, + entry: ['src/cli.ts'], + esmOptions: { outDir: 'dist', dts: false }, + target: 'es2022' + }), + attw: false, + publint: { enabled: true, level: 'error' } +}); diff --git a/packages/i18next-type-generator/vitest.config.ts b/packages/i18next-type-generator/vitest.config.ts new file mode 100644 index 0000000..3ccfa80 --- /dev/null +++ b/packages/i18next-type-generator/vitest.config.ts @@ -0,0 +1,4 @@ +import { defineProject, mergeConfig } from 'vitest/config'; +import configShared from '../../vitest.shared.js'; + +export default mergeConfig(configShared, defineProject({})); diff --git a/packages/shared-http-pieces/package.json b/packages/shared-http-pieces/package.json index 19e507f..2df075d 100644 --- a/packages/shared-http-pieces/package.json +++ b/packages/shared-http-pieces/package.json @@ -50,6 +50,7 @@ "watch": "tsdown --config-loader unrun --watch", "typecheck": "tsc -p ../../tsconfig.json", "lint": "oxlint src --fix", + "generate:i18n": "node ../i18next-type-generator/dist/cli.js ./src/locales/en-US/ ./src/@types/i18next.d.ts", "prepack": "pnpm run build" }, "dependencies": { @@ -63,6 +64,7 @@ "tslib": "^2.8.1" }, "devDependencies": { + "@wolfstar/i18next-type-generator": "workspace:^", "typescript": "~5.8.3" }, "engines": { diff --git a/packages/shared-http-pieces/src/@types/i18next-custom.d.ts b/packages/shared-http-pieces/src/@types/i18next-custom.d.ts new file mode 100644 index 0000000..2ed9965 --- /dev/null +++ b/packages/shared-http-pieces/src/@types/i18next-custom.d.ts @@ -0,0 +1,11 @@ +import 'i18next'; + +declare module 'i18next' { + interface CustomTypeOptions { + resources: { + 'commands/shared': { + infoEmbedDescription: string; + }; + }; + } +} diff --git a/packages/shared-http-pieces/src/@types/i18next.d.ts b/packages/shared-http-pieces/src/@types/i18next.d.ts new file mode 100644 index 0000000..1ef1f5e --- /dev/null +++ b/packages/shared-http-pieces/src/@types/i18next.d.ts @@ -0,0 +1,27 @@ +// 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/shared": { + infoName: "info"; + infoDescription: "Provides information about me, and links for adding the bot and joining the support server"; + infoFieldUptimeTitle: "Uptime"; + infoFieldUptimeValue: "โ€ข **Host**: {{host}}\nโ€ข **Client**: {{client}}"; + infoFieldServerUsageTitle: "Server Usage"; + infoFieldServerUsageValue: "โ€ข **CPU Usage**: {{cpu}}\nโ€ข **Memory**: {{heapUsed}}MB (Total: {{heapTotal}}MB)"; + infoButtonInvite: "Add me to your server!"; + infoButtonSupport: "Support server"; + infoButtonGitHub: "GitHub Repository"; + infoButtonDonate: "Donate"; + }; + }; + } +} diff --git a/packages/shared-http-pieces/src/commands/info.ts b/packages/shared-http-pieces/src/commands/info.ts index f00eac9..5b3c3af 100644 --- a/packages/shared-http-pieces/src/commands/info.ts +++ b/packages/shared-http-pieces/src/commands/info.ts @@ -1,6 +1,6 @@ import { EmbedBuilder, time, TimestampStyles } from '@discordjs/builders'; import { Command, container, RegisterCommand } from '@wolfstar/http-framework'; -import { applyLocalizedBuilder, getSupportedUserLanguageT, type TFunction } from '@wolfstar/http-framework-i18n'; +import { applyLocalizedBuilder, getSupportedUserLanguageName, getT, type TFunction } from '@wolfstar/http-framework-i18n'; import { ButtonStyle, ComponentType, @@ -10,48 +10,50 @@ import { type APIEmbedField } from 'discord-api-types/v10'; import { cpus, uptime, type CpuInfo } from 'node:os'; -import { LanguageKeys } from '../lib/i18n/LanguageKeys.js'; import { getInvite, getRepository } from '../lib/information.js'; +type TranslateFn = TFunction<'commands/shared'>; + @RegisterCommand((builder) => applyLocalizedBuilder(builder, 'commands/shared:info')) export class SharedCommand extends Command { public override chatInputRun(interaction: Command.ChatInputInteraction) { - const t = getSupportedUserLanguageT(interaction); + const lng = getSupportedUserLanguageName(interaction); + const t = getT(lng, 'commands/shared'); const embed = new EmbedBuilder() - .setDescription(t(LanguageKeys.Commands.Shared.InfoEmbedDescription)) - .addFields(this.getUptimeStatistics(t), this.getServerUsageStatistics(t)); + .setDescription(t('infoEmbedDescription')) + .addFields(this.getUptimeStatistics(t), this.getServerUsageStatistics(t, lng)); const components = this.getComponents(t); return interaction.reply({ embeds: [embed.toJSON()], components, flags: MessageFlags.Ephemeral }); } - private getUptimeStatistics(t: TFunction): APIEmbedField { + private getUptimeStatistics(t: TranslateFn): APIEmbedField { const now = Date.now(); const nowSeconds = Math.round(now / 1000); return { - name: t(LanguageKeys.Commands.Shared.InfoFieldUptimeTitle), - value: t(LanguageKeys.Commands.Shared.InfoFieldUptimeValue, { + name: t('infoFieldUptimeTitle'), + value: t('infoFieldUptimeValue', { host: time(Math.round(nowSeconds - uptime()), TimestampStyles.RelativeTime), client: time(Math.round(nowSeconds - process.uptime()), TimestampStyles.RelativeTime) }) }; } - private getServerUsageStatistics(t: TFunction): APIEmbedField { + private getServerUsageStatistics(t: TranslateFn, lng: string): APIEmbedField { const usage = process.memoryUsage(); return { - name: t(LanguageKeys.Commands.Shared.InfoFieldServerUsageTitle), - value: t(LanguageKeys.Commands.Shared.InfoFieldServerUsageValue, { + name: t('infoFieldServerUsageTitle'), + value: t('infoFieldServerUsageValue', { cpu: cpus().map(SharedCommand.formatCpuInfo.bind(null)).join(' | '), - heapUsed: (usage.heapUsed / 1048576).toLocaleString(t.lng, { maximumFractionDigits: 2 }), - heapTotal: (usage.heapTotal / 1048576).toLocaleString(t.lng, { maximumFractionDigits: 2 }) + heapUsed: (usage.heapUsed / 1048576).toLocaleString(lng, { maximumFractionDigits: 2 }), + heapTotal: (usage.heapTotal / 1048576).toLocaleString(lng, { maximumFractionDigits: 2 }) }) }; } - private getComponents(t: TFunction) { + private getComponents(t: TranslateFn) { const url = getInvite(); const support = this.getSupportComponent(t); const github = this.getGitHubComponent(t); @@ -67,41 +69,41 @@ export class SharedCommand extends Command { return { type: ComponentType.ActionRow, components }; } - private getSupportComponent(t: TFunction): APIComponentInMessageActionRow { + private getSupportComponent(t: TranslateFn): APIComponentInMessageActionRow { return { type: ComponentType.Button, style: ButtonStyle.Link, - label: t(LanguageKeys.Commands.Shared.InfoButtonSupport), + label: t('infoButtonSupport'), emoji: { name: '๐Ÿ†˜' }, url: 'https://discord.gg/6gakFR2' }; } - private getInviteComponent(t: TFunction, url: string): APIComponentInMessageActionRow { + private getInviteComponent(t: TranslateFn, url: string): APIComponentInMessageActionRow { return { type: ComponentType.Button, style: ButtonStyle.Link, - label: t(LanguageKeys.Commands.Shared.InfoButtonInvite), + label: t('infoButtonInvite'), emoji: { name: '๐ŸŽ‰' }, url }; } - private getGitHubComponent(t: TFunction): APIComponentInMessageActionRow { + private getGitHubComponent(t: TranslateFn): APIComponentInMessageActionRow { return { type: ComponentType.Button, style: ButtonStyle.Link, - label: t(LanguageKeys.Commands.Shared.InfoButtonGitHub), + label: t('infoButtonGitHub'), emoji: { id: '950888087188283422', name: 'github2' }, url: getRepository() }; } - private getDonateComponent(t: TFunction): APIComponentInMessageActionRow { + private getDonateComponent(t: TranslateFn): APIComponentInMessageActionRow { return { type: ComponentType.Button, style: ButtonStyle.Link, - label: t(LanguageKeys.Commands.Shared.InfoButtonDonate), + label: t('infoButtonDonate'), emoji: { name: '๐Ÿงก' }, url: 'https://donate.wolfstar.rocks' }; diff --git a/packages/shared-http-pieces/src/index.ts b/packages/shared-http-pieces/src/index.ts index 2e3e6aa..d5ce119 100644 --- a/packages/shared-http-pieces/src/index.ts +++ b/packages/shared-http-pieces/src/index.ts @@ -1,7 +1,6 @@ /// export * as Sentry from '@sentry/node'; -export * from './lib/i18n/LanguageKeys.js'; export * from './lib/information.js'; export * from './lib/sentry.js'; export * from './lib/utilities/register-command.js'; diff --git a/packages/shared-http-pieces/src/lib/i18n/LanguageKeys.ts b/packages/shared-http-pieces/src/lib/i18n/LanguageKeys.ts deleted file mode 100644 index b211b0a..0000000 --- a/packages/shared-http-pieces/src/lib/i18n/LanguageKeys.ts +++ /dev/null @@ -1 +0,0 @@ -export * as LanguageKeys from './LanguageKeys/All.js'; diff --git a/packages/shared-http-pieces/src/lib/i18n/LanguageKeys/All.ts b/packages/shared-http-pieces/src/lib/i18n/LanguageKeys/All.ts deleted file mode 100644 index b05ec06..0000000 --- a/packages/shared-http-pieces/src/lib/i18n/LanguageKeys/All.ts +++ /dev/null @@ -1 +0,0 @@ -export * as Commands from './Commands/All.js'; diff --git a/packages/shared-http-pieces/src/lib/i18n/LanguageKeys/Commands/All.ts b/packages/shared-http-pieces/src/lib/i18n/LanguageKeys/Commands/All.ts deleted file mode 100644 index 60bb033..0000000 --- a/packages/shared-http-pieces/src/lib/i18n/LanguageKeys/Commands/All.ts +++ /dev/null @@ -1 +0,0 @@ -export * as Shared from './Shared.js'; diff --git a/packages/shared-http-pieces/src/lib/i18n/LanguageKeys/Commands/Shared.ts b/packages/shared-http-pieces/src/lib/i18n/LanguageKeys/Commands/Shared.ts deleted file mode 100644 index a1e0c57..0000000 --- a/packages/shared-http-pieces/src/lib/i18n/LanguageKeys/Commands/Shared.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { FT, T } from '@wolfstar/http-framework-i18n'; - -export const DonateName = T('commands/shared:donateName'); -export const DonateDescription = T('commands/shared:donateDescription'); -export const DonateResponse = FT<{ urls: string }>('commands/shared:donateResponse'); -export const InfoName = T('commands/shared:inviteName'); -export const InfoDescription = T('commands/shared:inviteDescription'); -export const InfoEmbedDescription = T('commands/shared:infoEmbedDescription'); -export const InfoFieldUptimeTitle = T('commands/shared:infoFieldUptimeTitle'); -export const InfoFieldUptimeValue = FT<{ host: string; client: string }>('commands/shared:infoFieldUptimeValue'); -export const InfoFieldServerUsageTitle = T('commands/shared:infoFieldServerUsageTitle'); -export const InfoFieldServerUsageValue = FT<{ cpu: string; heapUsed: string; heapTotal: string }>('commands/shared:infoFieldServerUsageValue'); -export const InfoButtonInvite = T('commands/shared:infoButtonInvite'); -export const InfoButtonSupport = T('commands/shared:infoButtonSupport'); -export const InfoButtonGitHub = T('commands/shared:infoButtonGitHub'); -export const InfoButtonDonate = T('commands/shared:infoButtonDonate'); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc629de..4948471 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -161,8 +161,8 @@ importers: specifier: ^0.38.8 version: 0.38.48 i18next: - specifier: ^22.5.1 - version: 22.5.1 + specifier: ^25.10.10 + version: 25.10.10(typescript@5.8.3) tslib: specifier: ^2.8.1 version: 2.8.1 @@ -193,6 +193,44 @@ importers: specifier: ~5.8.3 version: 5.8.3 + packages/i18next-type-generator: + dependencies: + citty: + specifier: ^0.1.6 + version: 0.1.6 + colorette: + specifier: ^2.0.20 + version: 2.0.20 + tslib: + specifier: ^2.8.1 + version: 2.8.1 + devDependencies: + '@types/node': + specifier: 22.15.21 + version: 22.15.21 + '@vitest/coverage-v8': + specifier: ^4.1.8 + version: 4.1.8(vitest@4.1.8) + i18next: + specifier: ^25.10.10 + version: 25.10.10(typescript@5.8.3) + tsdown: + specifier: ^0.22.2 + version: 0.22.2(@arethetypeswrong/core@0.18.3)(publint@0.3.21)(typescript@5.8.3)(unrun@0.3.1) + typescript: + specifier: ~5.8.3 + version: 5.8.3 + vitest: + specifier: ^4.1.8 + version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@22.15.21)(@vitest/coverage-v8@4.1.8)(vite@8.0.14) + optionalDependencies: + oxfmt: + specifier: ^0.53.0 + version: 0.53.0 + prettier: + specifier: ^3.5.3 + version: 3.8.3 + packages/influx-utilities: dependencies: '@influxdata/influxdb-client': @@ -305,6 +343,9 @@ importers: specifier: ^2.8.1 version: 2.8.1 devDependencies: + '@wolfstar/i18next-type-generator': + specifier: workspace:^ + version: link:../i18next-type-generator typescript: specifier: ~5.8.3 version: 5.8.3 @@ -2291,9 +2332,6 @@ packages: engines: {node: '>=18'} hasBin: true - i18next@22.5.1: - resolution: {integrity: sha512-8TGPgM3pAD+VRsMtUMNknRz3kzqwp/gPALrWMsDnmC1mKqJwpWyooQRLMcbTwq8z8YwSmuj+ZYvc+xCuEpkssA==} - i18next@25.10.10: resolution: {integrity: sha512-cqUW2Z3EkRx7NqSyywjkgCLK7KLCL6IFVFcONG7nVYIJ3ekZ1/N5jUsihHV6Bq37NfhgtczxJcxduELtjTwkuQ==} peerDependencies: @@ -2885,6 +2923,11 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + proc-log@5.0.0: resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} engines: {node: ^18.17.0 || >=20.5.0} @@ -5342,10 +5385,6 @@ snapshots: husky@9.1.7: {} - i18next@22.5.1: - dependencies: - '@babel/runtime': 7.29.7 - i18next@25.10.10(typescript@5.8.3): dependencies: '@babel/runtime': 7.29.7 @@ -5896,6 +5935,9 @@ snapshots: prettier@2.8.8: {} + prettier@3.8.3: + optional: true + proc-log@5.0.0: {} promise-retry@2.0.1: diff --git a/scripts/clean.mjs b/scripts/clean.mjs index 71895a6..eb21499 100644 --- a/scripts/clean.mjs +++ b/scripts/clean.mjs @@ -10,6 +10,7 @@ const paths = [ new URL('http-framework/dist/', packagesDir), new URL('http-framework-i18n/dist/', packagesDir), new URL('i18next-backend/dist/', packagesDir), + new URL('i18next-type-generator/dist/', packagesDir), new URL('logger/dist/', packagesDir), new URL('safe-fetch/dist/', packagesDir), new URL('shared-http-pieces/dist/', packagesDir), @@ -25,6 +26,7 @@ const paths = [ new URL('http-framework/.turbo/', packagesDir), new URL('http-framework-i18n/.turbo/', packagesDir), new URL('i18next-backend/.turbo/', packagesDir), + new URL('i18next-type-generator/.turbo/', packagesDir), new URL('logger/.turbo/', packagesDir), new URL('safe-fetch/.turbo/', packagesDir), new URL('shared-http-pieces/.turbo/', packagesDir),