Skip to content
Merged
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
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,8 @@ export default defineConfig({

// TypeScript type generation
types: {
input: ['locales/en/*.json'],
input: ['locales/en/*.json'], // or use '**/*.json' with basePath for nested namespaces
basePath: 'locales/en', // Optional: enables nested directory structures as namespaces
output: 'src/types/i18next.d.ts',
resourcesFile: 'src/types/resources.d.ts',
enableSelector: true, // Enable type-safe key selection
Expand Down Expand Up @@ -1216,6 +1217,47 @@ export default {
} as const;
```

### Nested Namespaces for Type Generation

When generating TypeScript types, namespaces are derived from the filename only by default (e.g., `locales/en/dashboard/user.json` → namespace: `user`). If you organize translation files in nested directories and want the generated types to preserve that structure as part of the namespace, use the `basePath` option in your `types` configuration.

Configuration (`i18next.config.ts`):

```typescript
export default defineConfig({
locales: ['en', 'de'],
extract: {
input: ['src/**/*.{ts,tsx}'],
output: 'public/locales/{{language}}/{{namespace}}.json',
},
types: {
input: 'public/locales/en/**/*.json',
basePath: 'public/locales/en',
output: 'src/types/i18next.d.ts',
resourcesFile: 'src/types/resources.d.ts',
}
});
```

With this configuration:
- `public/locales/en/common.json` → type namespace: `common`
- `public/locales/en/dashboard/user.json` → type namespace: `dashboard/user`
- `public/locales/en/dashboard/settings.json` → type namespace: `dashboard/settings`
- `public/locales/en/features/auth/login.json` → type namespace: `features/auth/login`

The `basePath` can include the `{{language}}` placeholder for flexibility:

```typescript
types: {
input: 'public/locales/en/**/*.json',
basePath: 'public/locales/{{language}}',
output: 'src/types/i18next.d.ts',
resourcesFile: 'src/types/resources.d.ts',
}
```

This is useful for organizing translations into logical groups while maintaining type safety across your entire namespace hierarchy.

## Migration from i18next-parser

Automatically migrate from legacy `i18next-parser.config.js`:
Expand Down
32 changes: 26 additions & 6 deletions src/types-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { glob } from 'glob'
import { createSpinnerLike } from './utils/wrap-ora.js'
import { styleText } from 'node:util'
import { mkdir, readFile, writeFile, access } from 'node:fs/promises'
import { basename, extname, resolve, dirname, join, relative } from 'node:path'
import { basename, extname, resolve, dirname, join, relative, normalize } from 'node:path'
import { transform } from '@swc/core'
import type { I18nextToolkitConfig, Logger } from './types.js'
import { getOutputPath } from './utils/file-utils.js'
Expand Down Expand Up @@ -60,14 +60,32 @@ async function loadFile (file: string) {
return JSON.parse(content)
}

/**
* Extracts namespace from file path relative to base path.
* Preserves directory structure as namespace using forward slashes.
*
* @example
* getNamespaceFromPath('public/locales/en/dashboard/user.json', 'public/locales/en')
* // Returns: 'dashboard/user'
*/
function getNamespaceFromPath (filePath: string, basePath: string): string {
const normalized = normalize(filePath)
const normalizedBase = normalize(basePath)

let relativePath = relative(normalizedBase, normalized)
relativePath = relativePath.replace(extname(relativePath), '')

return relativePath.replace(/\\/g, '/')
}

/**
* Generates TypeScript type definitions for i18next translations.
*
* This function:
* 1. Reads translation files based on the input glob patterns
* 2. Generates TypeScript interfaces using i18next-resources-for-ts
* 3. Creates separate resources.d.ts and main i18next.d.ts files
* 4. Handles namespace detection from filenames
* 4. Handles namespace detection from filenames. Supports nested namespaces when `basePath` is provided
* 5. Supports type-safe selector API when enabled
*
* @param config - The i18next toolkit configuration object
Expand Down Expand Up @@ -108,14 +126,16 @@ export async function runTypesGenerator (
return
}

const resourceFiles = await glob(config.types?.input || [], {
cwd: process.cwd(),
})
const resourceFiles = await glob(config.types.input, { cwd: process.cwd() })
const basePath = config.types.basePath ? getOutputPath(config.types.basePath, config.extract.primaryLanguage) : undefined

const resources: Resource[] = []

for (const file of resourceFiles) {
const namespace = basename(file, extname(file))
const namespace = basePath
? getNamespaceFromPath(file, basePath)
: basename(file, extname(file))

const parsedContent = await loadFile(file)

// If mergeNamespaces is used, a single file can contain multiple namespaces
Expand Down
26 changes: 25 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,9 +261,33 @@ export interface I18nextToolkitConfig {

/** Configuration options for TypeScript type generation */
types?: {
/** Glob pattern(s) for translation files to generate types from */
/**
* Glob pattern(s) for translation files to generate types from.
*
* @example
* // Flat namespaces (filename only)
* 'public/locales/en/*.json'
*
* @example
* // Nested namespaces (requires basePath)
* input: 'public/locales/en/**\/*.json',
* basePath: 'public/locales/en'
*/
input: string | string[];

/**
* Base path for resolving namespaces from nested directory structures.
* When set, namespace is derived from the file's path relative to basePath.
*
* @example
* // Enable nested namespaces:
* input: 'public/locales/en/**\/*.json',
* basePath: 'public/locales/en'
* // 'public/locales/en/dashboard/user.json' → namespace: 'dashboard/user'
* // 'public/locales/en/common.json' → namespace: 'common'
*/
basePath?: string;

/** Output path for the main TypeScript definition file */
output: string;

Expand Down
Loading