messages
Learn how Duck Gen scans @duckgen tags and generates strongly-typed i18n dictionaries and message registries.
Overview
The messages extension scans source files for exported constants tagged with a @duckgen
JSDoc comment. It reads the keys from those constants and emits TypeScript types that
describe the message registry — compile-time safety for i18n dictionaries, error codes,
or any string-keyed data declared on the server.
What it scans
Duck Gen picks up exported const declarations that:
- Have a JSDoc comment with a
@duckgentag. - Are an array or object with
as const.
// Array source — values become message keys
/**
* @duckgen messages auth
*/
export const AuthMessages = [
'AUTH_SIGNIN_SUCCESS',
'AUTH_SIGNIN_FAILED',
'AUTH_USERNAME_INVALID',
] as const
// Object source — keys become message keys
/**
* @duckgen messages payments
*/
export const PaymentMessages = {
PAYMENT_SUCCESS: 'PAYMENT_SUCCESS',
PAYMENT_DECLINED: 'PAYMENT_DECLINED',
PAYMENT_INSUFFICIENT_FUNDS: 'PAYMENT_INSUFFICIENT_FUNDS',
} as const// Array source — values become message keys
/**
* @duckgen messages auth
*/
export const AuthMessages = [
'AUTH_SIGNIN_SUCCESS',
'AUTH_SIGNIN_FAILED',
'AUTH_USERNAME_INVALID',
] as const
// Object source — keys become message keys
/**
* @duckgen messages payments
*/
export const PaymentMessages = {
PAYMENT_SUCCESS: 'PAYMENT_SUCCESS',
PAYMENT_DECLINED: 'PAYMENT_DECLINED',
PAYMENT_INSUFFICIENT_FUNDS: 'PAYMENT_INSUFFICIENT_FUNDS',
} as constBoth formats produce the same result: typed message keys grouped under a name.
Tag format
Valid forms of the @duckgen JSDoc tag:
| Tag | Group key |
|---|---|
@duckgen | Uses the const name (e.g. AuthMessages) |
@duckgen messages | Uses the const name |
@duckgen message | Uses the const name |
@duckgen auth | Uses auth as the group key |
@duckgen messages auth | Uses auth as the group key |
@duckgen message auth | Uses auth as the group key |
When to pass a custom group key: Pass a custom key for a short scope name. Without one, the const variable name is used, which is often longer.
// Group key = "auth" (explicit)
/** @duckgen messages auth */
export const AuthMessages = [ ... ] as const
// Group key = "AuthMessages" (inferred from const name)
/** @duckgen */
export const AuthMessages = [ ... ] as const// Group key = "auth" (explicit)
/** @duckgen messages auth */
export const AuthMessages = [ ... ] as const
// Group key = "AuthMessages" (inferred from const name)
/** @duckgen */
export const AuthMessages = [ ... ] as constGroup key rules
- Each group key must be unique project-wide.
- Two constants with the same group key — the duplicate is skipped with a warning.
- Two constants with the same variable name — the duplicate is skipped with a warning.
- Group keys are case-sensitive:
authandAuthare different groups.
The as const requirement
as const is required on the array or object. Without it, TypeScript widens the types to
string[] or Record<string, string> and Duck Gen can't read the literal keys.
// Without as const — keys become just `string`
export const Messages = ['MSG_ONE', 'MSG_TWO']
// With as const — keys are literal types 'MSG_ONE' | 'MSG_TWO'
export const Messages = ['MSG_ONE', 'MSG_TWO'] as const// Without as const — keys become just `string`
export const Messages = ['MSG_ONE', 'MSG_TWO']
// With as const — keys are literal types 'MSG_ONE' | 'MSG_TWO'
export const Messages = ['MSG_ONE', 'MSG_TWO'] as constArray vs object sources
Array source
Use arrays when the keys are plain identifiers:
/** @duckgen messages auth */
export const AuthMessages = [
'AUTH_SIGNIN_SUCCESS',
'AUTH_SIGNIN_FAILED',
'AUTH_USERNAME_INVALID',
'AUTH_PASSWORD_INVALID',
] as const/** @duckgen messages auth */
export const AuthMessages = [
'AUTH_SIGNIN_SUCCESS',
'AUTH_SIGNIN_FAILED',
'AUTH_USERNAME_INVALID',
'AUTH_PASSWORD_INVALID',
] as constThe array values become the message keys. The generated type maps each key to a string
(the translation text).
Object source
Use objects to attach status codes or other metadata to each key:
/** @duckgen messages auth */
export const AuthMessages = {
AUTH_SIGNIN_SUCCESS: 200,
AUTH_SIGNIN_FAILED: 500,
AUTH_USERNAME_INVALID: 401,
AUTH_PASSWORD_INVALID: 401,
} as const/** @duckgen messages auth */
export const AuthMessages = {
AUTH_SIGNIN_SUCCESS: 200,
AUTH_SIGNIN_FAILED: 500,
AUTH_USERNAME_INVALID: 401,
AUTH_PASSWORD_INVALID: 401,
} as constThe object keys become the message keys. Values can be anything — Duck Gen only uses the keys.
Mixing shared constants
Spread shared constants into an array:
export const ZodMessages = {
ZOD_EXPECTED_STRING: 'ZOD_EXPECTED_STRING',
ZOD_TOO_LONG: 'ZOD_TOO_LONG',
ZOD_TOO_SHORT: 'ZOD_TOO_SHORT',
} as constexport const ZodMessages = {
ZOD_EXPECTED_STRING: 'ZOD_EXPECTED_STRING',
ZOD_TOO_LONG: 'ZOD_TOO_LONG',
ZOD_TOO_SHORT: 'ZOD_TOO_SHORT',
} as constimport { ZodMessages } from '~/common/constants'
/** @duckgen messages auth */
export const AuthMessages = [
'AUTH_SIGNIN_SUCCESS',
'AUTH_SIGNIN_FAILED',
...Object.values(ZodMessages),
] as constimport { ZodMessages } from '~/common/constants'
/** @duckgen messages auth */
export const AuthMessages = [
'AUTH_SIGNIN_SUCCESS',
'AUTH_SIGNIN_FAILED',
...Object.values(ZodMessages),
] as constThe auth i18n dictionary now covers both auth-specific and shared validation messages.
Generated types
The messages extension writes a .d.ts file with these types:
DuckgenMessageSources
A type registry mapping group keys to their source types:
type DuckgenMessageSources = {
auth: typeof AuthMessages
payments: typeof PaymentMessages
}type DuckgenMessageSources = {
auth: typeof AuthMessages
payments: typeof PaymentMessages
}DuckgenMessageGroup
A union of all group keys:
type DuckgenMessageGroup = 'auth' | 'payments'type DuckgenMessageGroup = 'auth' | 'payments'DuckgenMessageKey
Message keys for a group, or across all groups:
// Keys for a specific group
type AuthKey = DuckgenMessageKey<'auth'>
// => 'AUTH_SIGNIN_SUCCESS' | 'AUTH_SIGNIN_FAILED' | ...
// All keys across all groups
type AllKeys = DuckgenMessageKey
// => 'AUTH_SIGNIN_SUCCESS' | ... | 'PAYMENT_SUCCESS' | ...// Keys for a specific group
type AuthKey = DuckgenMessageKey<'auth'>
// => 'AUTH_SIGNIN_SUCCESS' | 'AUTH_SIGNIN_FAILED' | ...
// All keys across all groups
type AllKeys = DuckgenMessageKey
// => 'AUTH_SIGNIN_SUCCESS' | ... | 'PAYMENT_SUCCESS' | ...DuckgenMessageDictionary
A record mapping a group's message keys to string translations:
type AuthDictionary = DuckgenMessageDictionary<'auth'>
// => { AUTH_SIGNIN_SUCCESS: string; AUTH_SIGNIN_FAILED: string; ... }type AuthDictionary = DuckgenMessageDictionary<'auth'>
// => { AUTH_SIGNIN_SUCCESS: string; AUTH_SIGNIN_FAILED: string; ... }DuckgenMessageDictionaryByGroup
All groups mapped to their dictionaries:
type AllDicts = DuckgenMessageDictionaryByGroup
// => { auth: { AUTH_SIGNIN_SUCCESS: string; ... }; payments: { PAYMENT_SUCCESS: string; ... } }type AllDicts = DuckgenMessageDictionaryByGroup
// => { auth: { AUTH_SIGNIN_SUCCESS: string; ... }; payments: { PAYMENT_SUCCESS: string; ... } }DuckGenI18nMessages
Alias for DuckgenMessageDictionaryByGroup. Pick whichever name reads better.
DuckgenI18n
Maps language codes to a dictionary for one or more groups:
type AuthI18n = DuckgenI18n<'en' | 'ar', 'auth'>
// => { en: { AUTH_SIGNIN_SUCCESS: string; ... }; ar: { AUTH_SIGNIN_SUCCESS: string; ... } }type AuthI18n = DuckgenI18n<'en' | 'ar', 'auth'>
// => { en: { AUTH_SIGNIN_SUCCESS: string; ... }; ar: { AUTH_SIGNIN_SUCCESS: string; ... } }DuckgenI18nByGroup
Maps language codes to all group dictionaries:
type FullI18n = DuckgenI18nByGroup<'en' | 'ar'>
// => { en: { auth: { ... }; payments: { ... } }; ar: { auth: { ... }; payments: { ... } } }type FullI18n = DuckgenI18nByGroup<'en' | 'ar'>
// => { en: { auth: { ... }; payments: { ... } }; ar: { auth: { ... }; payments: { ... } } }DuckgenScopedI18nByGroup
Wraps all groups under a server scope. Use this when the app mixes server-generated
and client-only messages:
type ScopedI18n = DuckgenScopedI18nByGroup<'en' | 'ar', DuckGenI18nMessages>
// => { en: { server: { AuthMessages: { ... }; PaymentMessages: { ... } } }; ar: { ... } }type ScopedI18n = DuckgenScopedI18nByGroup<'en' | 'ar', DuckGenI18nMessages>
// => { en: { server: { AuthMessages: { ... }; PaymentMessages: { ... } } }; ar: { ... } }Real-world i18n example
A type-safe i18n system built on generated message types:
Step 1: Define messages on the server
/** @duckgen messages auth */
export const AuthMessages = [
'AUTH_SIGNIN_SUCCESS',
'AUTH_SIGNIN_FAILED',
'AUTH_USERNAME_INVALID',
'AUTH_PASSWORD_INVALID',
] as const/** @duckgen messages auth */
export const AuthMessages = [
'AUTH_SIGNIN_SUCCESS',
'AUTH_SIGNIN_FAILED',
'AUTH_USERNAME_INVALID',
'AUTH_PASSWORD_INVALID',
] as constStep 2: Run Duck Gen
bunx duck-genbunx duck-genStep 3: Build the i18n object on the client
import type { DuckgenScopedI18nByGroup, DuckGenI18nMessages } from '@gentleduck/gen/nestjs'
type SupportedLang = 'en' | 'ar'
type I18n = DuckgenScopedI18nByGroup<SupportedLang, DuckGenI18nMessages>
export const i18n: I18n = {
en: {
server: {
AuthMessages: {
AUTH_SIGNIN_SUCCESS: 'Signed in successfully',
AUTH_SIGNIN_FAILED: 'Sign in failed',
AUTH_USERNAME_INVALID: 'Invalid username',
AUTH_PASSWORD_INVALID: 'Invalid password',
},
},
},
ar: {
server: {
AuthMessages: {
AUTH_SIGNIN_SUCCESS: 'تم تسجيل الدخول بنجاح',
AUTH_SIGNIN_FAILED: 'فشل تسجيل الدخول',
AUTH_USERNAME_INVALID: 'اسم المستخدم غير صحيح',
AUTH_PASSWORD_INVALID: 'كلمة المرور غير صحيحة',
},
},
},
}import type { DuckgenScopedI18nByGroup, DuckGenI18nMessages } from '@gentleduck/gen/nestjs'
type SupportedLang = 'en' | 'ar'
type I18n = DuckgenScopedI18nByGroup<SupportedLang, DuckGenI18nMessages>
export const i18n: I18n = {
en: {
server: {
AuthMessages: {
AUTH_SIGNIN_SUCCESS: 'Signed in successfully',
AUTH_SIGNIN_FAILED: 'Sign in failed',
AUTH_USERNAME_INVALID: 'Invalid username',
AUTH_PASSWORD_INVALID: 'Invalid password',
},
},
},
ar: {
server: {
AuthMessages: {
AUTH_SIGNIN_SUCCESS: 'تم تسجيل الدخول بنجاح',
AUTH_SIGNIN_FAILED: 'فشل تسجيل الدخول',
AUTH_USERNAME_INVALID: 'اسم المستخدم غير صحيح',
AUTH_PASSWORD_INVALID: 'كلمة المرور غير صحيحة',
},
},
},
}TypeScript errors when you:
- Misspell a message key.
- Miss a translation for a key.
- Add a key that isn't in the server constants.
Step 4: Use in the UI
import { i18n } from './i18n'
function showSigninError(lang: 'en' | 'ar', messageKey: string) {
const text = i18n[lang].server.AuthMessages[messageKey]
toast.error(text)
}import { i18n } from './i18n'
function showSigninError(lang: 'en' | 'ar', messageKey: string) {
const text = i18n[lang].server.AuthMessages[messageKey]
toast.error(text)
}Multiple message groups
Define as many groups as you need. Each module gets its own:
/** @duckgen messages auth */
export const AuthMessages = ['AUTH_SIGNIN_SUCCESS', 'AUTH_SIGNIN_FAILED'] as const/** @duckgen messages auth */
export const AuthMessages = ['AUTH_SIGNIN_SUCCESS', 'AUTH_SIGNIN_FAILED'] as const/** @duckgen messages users */
export const UserMessages = ['USER_CREATED', 'USER_NOT_FOUND', 'USER_UPDATED'] as const/** @duckgen messages users */
export const UserMessages = ['USER_CREATED', 'USER_NOT_FOUND', 'USER_UPDATED'] as const/** @duckgen messages payments */
export const PaymentMessages = ['PAYMENT_SUCCESS', 'PAYMENT_DECLINED'] as const/** @duckgen messages payments */
export const PaymentMessages = ['PAYMENT_SUCCESS', 'PAYMENT_DECLINED'] as constAll three groups land in the generated types, accessible individually or together.
Troubleshooting
| Problem | Solution |
|---|---|
Keys are just string | Add as const to the array or object. |
| Group missing | The constant must be exported and have a @duckgen JSDoc tag. |
| Duplicate group warning | Two constants share the same group key — rename one. |
| Duplicate const name | Two files export the same name — rename one. |
| Spread values missing | Duck Gen reads literal values only. Spreading a dynamic expression may lose them. Put as const on the source too. |
Next steps
- Generated types reference: full type API for messages.
- Templates: complete example with messages, controllers, and Duck Query.
- Duck Query: pair your types with a type-safe HTTP client.