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
15 changes: 15 additions & 0 deletions docs/src/pages/docs/workflows/typescript.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,21 @@ declare module 'next-intl' {
}
```

If some of your locales have different keys (e.g., a key exists in `en.json` but is missing in `es.json`), you can use a union type to catch these inconsistencies at the call site.
With this setup, only keys that are present in **all** locale files are accessible via the translator — using a key missing in any locale will result in a TypeScript error.

```ts filename="global.ts"
import messagesEn from './messages/en.json';
import messagesEs from './messages/es.json';

declare module 'next-intl' {
interface AppConfig {
// ...
Messages: typeof messagesEn | typeof messagesEs;
}
}
```

You can freely define the interface, but if you have your messages available locally, it can be helpful to automatically create the type based on the messages from your default locale.

<Details id="messages-performance-tsc">
Expand Down
16 changes: 9 additions & 7 deletions packages/use-intl/src/core/MessageKeys.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export type NestedKeyOf<ObjectType> = ObjectType extends object
export type NestedKeyOf<ObjectType> = [ObjectType] extends [object]
? {
[Property in keyof ObjectType]:
| `${Property & string}`
| `${Property & string}.${NestedKeyOf<ObjectType[Property]>}`;
}[keyof ObjectType]
[Property in keyof ObjectType & string]:
| Property
| `${Property}.${NestedKeyOf<ObjectType[Property]>}`;
}[keyof ObjectType & string]
: never;

export type NestedValueOf<
Expand All @@ -30,7 +30,9 @@ export type MessageKeys<ObjectType, AllKeys extends string> = {
[PropertyPath in AllKeys]: NestedValueOf<
ObjectType,
PropertyPath
> extends string
? PropertyPath
> extends infer V
? V extends string
? PropertyPath
: never
: never;
}[AllKeys];
69 changes: 69 additions & 0 deletions packages/use-intl/src/core/createTranslator.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,75 @@ describe('type safety', () => {
});
});

describe('keys, union messages type', () => {
type MessagesEn = {meta: {title: 'title'; description: 'description'}};
type MessagesEs = {meta: {title: 'title-es'}};
type MessagesUnion = MessagesEn | MessagesEs;

it('allows keys present in all union members', () => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
() => {
const t = createTranslator<MessagesUnion>({
locale: 'en',
messages: {meta: {title: 'title', description: 'description'}}
});
t('meta.title');

const tMeta = createTranslator<MessagesUnion, 'meta'>({
locale: 'en',
namespace: 'meta',
messages: {meta: {title: 'title', description: 'description'}}
});
tMeta('title');
};
});

it('requires params from all union members when a key has different args', () => {
type MessagesWithParamsEn = {title: 'Hello {name}'};
type MessagesWithParamsEs = {title: 'Hola {count, number}'};
type MessagesWithParamsUnion =
| MessagesWithParamsEn
| MessagesWithParamsEs;

// eslint-disable-next-line @typescript-eslint/no-unused-expressions
() => {
const t = createTranslator<MessagesWithParamsUnion>({
locale: 'en',
messages: {title: 'Hello {name}'}
});

// Providing all params from all union members is valid
t('title', {name: 'John', count: 5});

// Providing only some params is an error
// @ts-expect-error
t('title', {name: 'John'});
// @ts-expect-error
t('title', {count: 5});
};
});

it('disallows keys missing in some union members', () => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
() => {
const t = createTranslator<MessagesUnion>({
locale: 'en',
messages: {meta: {title: 'title', description: 'description'}}
});
// @ts-expect-error
t('meta.description');

const tMeta = createTranslator<MessagesUnion, 'meta'>({
locale: 'en',
namespace: 'meta',
messages: {meta: {title: 'title', description: 'description'}}
});
// @ts-expect-error
tMeta('description');
};
});
});

describe('params, strictly-typed', () => {
function translateMessage<const T extends string>(msg: T) {
return createTranslator({
Expand Down
Loading